diff --git a/extensions/machine-learning-services/images/dark/delete_inverse.svg b/extensions/machine-learning-services/images/dark/delete_inverse.svg new file mode 100644 index 0000000000..7274a63148 --- /dev/null +++ b/extensions/machine-learning-services/images/dark/delete_inverse.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/extensions/machine-learning-services/images/dark/edit_inverse.svg b/extensions/machine-learning-services/images/dark/edit_inverse.svg new file mode 100644 index 0000000000..da956cb2c6 --- /dev/null +++ b/extensions/machine-learning-services/images/dark/edit_inverse.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/extensions/machine-learning-services/images/light/delete.svg b/extensions/machine-learning-services/images/light/delete.svg new file mode 100644 index 0000000000..548f3729d0 --- /dev/null +++ b/extensions/machine-learning-services/images/light/delete.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/extensions/machine-learning-services/images/light/edit.svg b/extensions/machine-learning-services/images/light/edit.svg new file mode 100644 index 0000000000..ecde924084 --- /dev/null +++ b/extensions/machine-learning-services/images/light/edit.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/extensions/machine-learning-services/package.json b/extensions/machine-learning-services/package.json index d4119b51ff..8946bfe73d 100644 --- a/extensions/machine-learning-services/package.json +++ b/extensions/machine-learning-services/package.json @@ -57,6 +57,10 @@ "command": "mls.command.managePackages", "title": "%mls.command.managePackages%" }, + { + "command": "mls.command.manageLanguages", + "title": "%mls.command.manageLanguages%" + }, { "command": "mls.command.odbcdriver", "title": "%mls.command.odbcdriver%" @@ -96,6 +100,7 @@ "widget": { "tasks-widget": [ "mls.command.managePackages", + "mls.command.manageLanguages", "mls.command.odbcdriver", "mls.command.mlsdocs" ] diff --git a/extensions/machine-learning-services/package.nls.json b/extensions/machine-learning-services/package.nls.json index cf723b3dc4..559633faa3 100644 --- a/extensions/machine-learning-services/package.nls.json +++ b/extensions/machine-learning-services/package.nls.json @@ -5,6 +5,7 @@ "title.configurations": "Configurations", "title.endpoints": "Endpoints", "mls.command.managePackages": "Manage Packages in SQL Server", + "mls.command.manageLanguages": "Manage External Languages", "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 c2594a4d27..468df64837 100644 --- a/extensions/machine-learning-services/src/common/apiWrapper.ts +++ b/extensions/machine-learning-services/src/common/apiWrapper.ts @@ -73,4 +73,16 @@ export class ApiWrapper { public getConfiguration(section?: string, resource?: vscode.Uri | null): vscode.WorkspaceConfiguration { return vscode.workspace.getConfiguration(section, resource); } + + public createTab(title: string): azdata.window.DialogTab { + return azdata.window.createTab(title); + } + + public createModelViewDialog(title: string, dialogName?: string, isWide?: boolean): azdata.window.Dialog { + return azdata.window.createModelViewDialog(title, dialogName, isWide); + } + + public openDialog(dialog: azdata.window.Dialog): void { + return azdata.window.openDialog(dialog); + } } diff --git a/extensions/machine-learning-services/src/common/constants.ts b/extensions/machine-learning-services/src/common/constants.ts index 4171cba035..ec0c0518ba 100644 --- a/extensions/machine-learning-services/src/common/constants.ts +++ b/extensions/machine-learning-services/src/common/constants.ts @@ -21,6 +21,7 @@ export const notebookExtensionName = 'Microsoft.notebook'; // Tasks, commands // +export const mlManageLanguagesCommand = 'mls.command.manageLanguages'; export const mlManagePackagesCommand = 'mls.command.managePackages'; export const mlOdbcDriverCommand = 'mls.command.odbcdriver'; export const mlsDocumentsCommand = 'mls.command.mlsdocs'; @@ -44,6 +45,7 @@ export const installDependenciesPackagesAlreadyInstalled = localize('mls.install export function installDependenciesGetPackagesError(err: string): string { return localize('mls.installDependencies.getPackagesError', "Failed to get installed python packages. Error: {0}", err); } export const packageManagerNoConnection = localize('mls.packageManager.NoConnection', "No connection selected"); export const notebookExtensionNotLoaded = localize('mls.notebookExtensionNotLoaded', "Notebook extension is not loaded"); +export const mssqlExtensionNotLoaded = localize('mls.mssqlExtensionNotLoaded', "MSSQL extension is not loaded"); export const mlsEnabledMessage = localize('mls.enabledMessage', "Machine Learning Services Enabled"); export const mlsDisabledMessage = localize('mls.disabledMessage', "Machine Learning Services Disabled"); export const mlsConfigUpdateFailed = localize('mls.configUpdateFailed', "Failed to modify Machine Learning Services configurations"); @@ -62,12 +64,36 @@ export const rConfigError = localize('mls.rConfigError', "R executable is not co export const installingDependencies = localize('mls.installingDependencies', "Installing dependencies ..."); export const resourceNotFoundError = localize('mls.resourceNotFound', "Could not find the specified resource"); export const latestVersion = localize('mls.latestVersion', "Latest"); +export const localhost = 'localhost'; export function httpGetRequestError(code: number, message: string): string { return localize('mls.httpGetRequestError', "Package info request failed with error: {0} {1}", code, message); } - +export function getErrorMessage(error: Error): string { return localize('azure.resource.error', "Error: {0}", error?.message); } +export const extLangInstallTabTitle = localize('extLang.installTabTitle', "Installed"); +export const extLangLanguageCreatedDate = localize('extLang.languageCreatedDate', "Installed"); +export const extLangLanguagePlatform = localize('extLang.languagePlatform', "Platform"); +export const deleteTitle = localize('extLang.delete', "Delete"); +export const extLangInstallButtonText = localize('extLang.installButtonText', "Install"); +export const extLangCancelButtonText = localize('extLang.CancelButtonText', "Cancel"); +export const extLangDoneButtonText = localize('extLang.DoneButtonText', "Done"); +export const extLangOkButtonText = localize('extLang.OkButtonText', "OK"); +export const extLangSaveButtonText = localize('extLang.SaveButtonText', "Save"); +export const extLangLanguageName = localize('extLang.languageName', "Name"); +export const extLangNewLanguageTabTitle = localize('extLang.newLanguageTabTitle', "Add new"); +export const extLangFileBrowserTabTitle = localize('extLang.fileBrowserTabTitle', "File Browser"); +export const extLangDialogTitle = localize('extLang.DialogTitle', "Languages"); +export const extLangTarget = localize('extLang.Target', "Target"); +export const extLangLocal = localize('extLang.Local', "localhost"); +export const extLangExtensionFilePath = localize('extLang.extensionFilePath', "Language extension path"); +export const extLangExtensionFileLocation = localize('extLang.extensionFileLocation', "Language extension location"); +export const extLangExtensionFileName = localize('extLang.extensionFileName', "Extension file Name"); +export const extLangEnvVariables = localize('extLang.envVariables', "Environment variables"); +export const extLangParameters = localize('extLang.parameters', "Parameters"); +export const extLangSelectedPath = localize('extLang.selectedPath', "Selected Path"); +export const extLangInstallFailedError = localize('extLang.installFailedError', "Failed to install language"); +export const extLangUpdateFailedError = localize('extLang.updateFailedError', "Failed to update language"); // Links // diff --git a/extensions/machine-learning-services/src/controllers/mainController.ts b/extensions/machine-learning-services/src/controllers/mainController.ts index 60ca25cb81..bb91f661e0 100644 --- a/extensions/machine-learning-services/src/controllers/mainController.ts +++ b/extensions/machine-learning-services/src/controllers/mainController.ts @@ -6,6 +6,7 @@ import * as vscode from 'vscode'; import * as nbExtensionApis from '../typings/notebookServices'; +import * as mssql from '../../../mssql'; import { PackageManager } from '../packageManagement/packageManager'; import * as constants from '../common/constants'; import { ApiWrapper } from '../common/apiWrapper'; @@ -15,6 +16,8 @@ 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 { LanguageService } from '../externalLanguage/languageService'; /** * The main controller class that initializes the extension @@ -65,6 +68,18 @@ export default class MainController implements vscode.Disposable { } } + /** + * Returns an instance of Server Installation from notebook extension + */ + private async getLanguageExtensionService(): Promise { + let mssqlExtension = this._apiWrapper.getExtension(mssql.extension.name)?.exports as mssql.IExtension; + if (mssqlExtension) { + return (mssqlExtension.languageExtension); + } else { + throw new Error(constants.mssqlExtensionNotLoaded); + } + } + private async initialize(): Promise { this._outputChannel.show(true); @@ -78,12 +93,23 @@ export default class MainController implements vscode.Disposable { this._apiWrapper.registerCommand(constants.mlManagePackagesCommand, (async () => { await packageManager.managePackages(); })); + + let mssqlService = await this.getLanguageExtensionService(); + let languagesModel = new LanguageService(this._apiWrapper, mssqlService); + let languageController = new LanguageController(this._apiWrapper, this._rootPath, languagesModel); + + this._apiWrapper.registerCommand(constants.mlManageLanguagesCommand, (async () => { + await languageController.manageLanguages(); + })); this._apiWrapper.registerCommand(constants.mlsDependenciesCommand, (async () => { await packageManager.installDependencies(); })); this._apiWrapper.registerTaskHandler(constants.mlManagePackagesCommand, async () => { await packageManager.managePackages(); }); + this._apiWrapper.registerTaskHandler(constants.mlManageLanguagesCommand, async () => { + await languageController.manageLanguages(); + }); this._apiWrapper.registerTaskHandler(constants.mlOdbcDriverCommand, async () => { await this.serverConfigManager.openOdbcDriverDocuments(); }); @@ -126,7 +152,6 @@ export default class MainController implements vscode.Disposable { return this._httpClient; } - /** * Config instance */ diff --git a/extensions/machine-learning-services/src/externalLanguage/languageController.ts b/extensions/machine-learning-services/src/externalLanguage/languageController.ts new file mode 100644 index 0000000000..920c5e2439 --- /dev/null +++ b/extensions/machine-learning-services/src/externalLanguage/languageController.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 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'; + +export class LanguageController { + + /** + * + */ + constructor( + private _apiWrapper: ApiWrapper, + private _root: string, + private _service: LanguageService) { + } + + /** + * Opens the manage language dialog and connects events to the model + */ + public async manageLanguages(): Promise { + + let dialog = new LanguagesDialog(this._apiWrapper, this._root); + + // Load current connection + // + await this._service.load(); + dialog.connection = this._service.connection; + dialog.connectionUrl = this._service.connectionUrl; + + // Handle dialog events and connect to model + // + dialog.onEdit(model => { + this.editLanguage(dialog, model); + }); + dialog.onDelete(async deleteModel => { + try { + await this.executeAction(dialog, this.deleteLanguage, this._service, deleteModel); + dialog.onUpdatedLanguage(deleteModel); + } catch (err) { + dialog.onActionFailed(err); + } + }); + + dialog.onUpdate(async updateModel => { + try { + await this.executeAction(dialog, this.updateLanguage, this._service, updateModel); + dialog.onUpdatedLanguage(updateModel); + } catch (err) { + dialog.onActionFailed(err); + } + }); + + dialog.onList(async () => { + try { + let result = await this.listLanguages(this._service); + dialog.onListLanguageLoaded(result); + } catch (err) { + dialog.onActionFailed(err); + } + }); + this.onSelectFile(dialog); + + // Open dialog + // + dialog.showDialog(); + return dialog; + } + + public async executeAction(dialog: LanguageViewBase, func: (...args: any[]) => Promise, ...args: any[]): Promise { + let result = await func(...args); + await dialog.reset(); + return result; + } + + public editLanguage(parent: LanguageViewBase, languageUpdateModel: LanguageUpdateModel): void { + let editDialog = new LanguageEditDialog(this._apiWrapper, parent, languageUpdateModel); + editDialog.showDialog(); + } + + private onSelectFile(dialog: LanguageViewBase): void { + dialog.fileBrowser(async (args) => { + let filePath = ''; + if (args.target === constants.localhost) { + filePath = await this.getLocalFilePath(); + + } else { + filePath = await this.getServerFilePath(args.target); + } + dialog.onFilePathSelected({ filePath: filePath, target: args.target }); + }); + } + + public getServerFilePath(connectionUrl: string): Promise { + return new Promise((resolve) => { + let dialog = new FileBrowserDialog(this._apiWrapper, connectionUrl); + dialog.onPathSelected((selectedPath) => { + resolve(selectedPath); + }); + + dialog.showDialog(); + }); + } + + public async getLocalFilePath(): Promise { + let result = await this._apiWrapper.showOpenDialog({ + canSelectFiles: true, + canSelectFolders: false, + canSelectMany: false + }); + return result && result.length > 0 ? result[0].fsPath : ''; + } + + public async deleteLanguage(model: LanguageService, deleteModel: LanguageUpdateModel): Promise { + await model.deleteLanguage(deleteModel.language.name); + } + + public async listLanguages(model: LanguageService): Promise { + return await model.getLanguageList(); + } + + public async updateLanguage(model: LanguageService, updateModel: LanguageUpdateModel): Promise { + if (!updateModel.language) { + return; + } + let contents: mssql.ExternalLanguageContent[] = []; + if (updateModel.language.contents && updateModel.language.contents.length >= 0) { + contents = updateModel.language.contents.filter(x => x.platform !== updateModel.content.platform); + } + contents.push(updateModel.content); + + updateModel.language.contents = contents; + await model.updateLanguage(updateModel.language); + } +} diff --git a/extensions/machine-learning-services/src/externalLanguage/languageService.ts b/extensions/machine-learning-services/src/externalLanguage/languageService.ts new file mode 100644 index 0000000000..c8a8e289a9 --- /dev/null +++ b/extensions/machine-learning-services/src/externalLanguage/languageService.ts @@ -0,0 +1,59 @@ +/*--------------------------------------------------------------------------------------------- + * 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 mssql from '../../../mssql/src/mssql'; +import { ApiWrapper } from '../common/apiWrapper'; + +/** + * Manage package dialog model + */ +export class LanguageService { + + public connection: azdata.connection.ConnectionProfile | undefined; + public connectionUrl: string = ''; + + constructor( + private _apiWrapper: ApiWrapper, + private _languageExtensionService: mssql.ILanguageExtensionService) { + } + + public async load() { + this.connection = await this.getCurrentConnection(); + this.connectionUrl = await this.getCurrentConnectionUrl(); + } + + public async getLanguageList(): Promise { + if (this.connectionUrl) { + return await this._languageExtensionService.listLanguages(this.connectionUrl); + } + + return []; + } + + public async deleteLanguage(languageName: string): Promise { + if (this.connectionUrl) { + await this._languageExtensionService.deleteLanguage(this.connectionUrl, languageName); + } + } + + public async updateLanguage(language: mssql.ExternalLanguage): Promise { + if (this.connectionUrl) { + await this._languageExtensionService.updateLanguage(this.connectionUrl, language); + } + } + + 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(); + } +} diff --git a/extensions/machine-learning-services/src/test/views/externalLanguages/addEditLanguageTab.test.ts b/extensions/machine-learning-services/src/test/views/externalLanguages/addEditLanguageTab.test.ts new file mode 100644 index 0000000000..3f0eefa622 --- /dev/null +++ b/extensions/machine-learning-services/src/test/views/externalLanguages/addEditLanguageTab.test.ts @@ -0,0 +1,120 @@ +/*--------------------------------------------------------------------------------------------- + * 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, ParentDialog } from './utils'; +import { AddEditLanguageTab } from '../../../views/externalLanguages/addEditLanguageTab'; +import { LanguageUpdateModel } from '../../../views/externalLanguages/languageViewBase'; + +describe('Add Edit External Languages Tab', () => { + it('Should create AddEditLanguageTab for new language successfully ', async function (): Promise { + let testContext = createContext(); + let parent = new ParentDialog(testContext.apiWrapper.object); + let languageUpdateModel: LanguageUpdateModel = { + content: parent.createNewContent(), + language: parent.createNewLanguage(), + newLang: true + }; + let tab = new AddEditLanguageTab(testContext.apiWrapper.object, parent, languageUpdateModel); + should.notEqual(tab.languageView, undefined, 'Failed to create language view for add'); + }); + + it('Should create AddEditLanguageTab for edit successfully ', async function (): Promise { + let testContext = createContext(); + let parent = new ParentDialog(testContext.apiWrapper.object); + let languageUpdateModel: LanguageUpdateModel = { + content: { + extensionFileName: 'filename', + isLocalFile: true, + pathToExtension: 'path', + }, + language: { + name: 'name', + contents: [] + }, + newLang: false + }; + let tab = new AddEditLanguageTab(testContext.apiWrapper.object, parent, languageUpdateModel); + should.notEqual(tab.languageView, undefined, 'Failed to create language view for edit'); + should.equal(tab.saveButton, undefined); + }); + + it('Should reset AddEditLanguageTab successfully ', async function (): Promise { + let testContext = createContext(); + let parent = new ParentDialog(testContext.apiWrapper.object); + let languageUpdateModel: LanguageUpdateModel = { + content: { + extensionFileName: 'filename', + isLocalFile: true, + pathToExtension: 'path', + }, + language: { + name: 'name', + contents: [] + }, + newLang: false + }; + let tab = new AddEditLanguageTab(testContext.apiWrapper.object, parent, languageUpdateModel); + if (tab.languageName) { + tab.languageName.value = 'some value'; + } + await tab.reset(); + should.equal(tab.languageName?.value, 'name'); + }); + + it('Should load content successfully ', async function (): Promise { + let testContext = createContext(); + let parent = new ParentDialog(testContext.apiWrapper.object); + let languageUpdateModel: LanguageUpdateModel = { + content: { + extensionFileName: 'filename', + isLocalFile: true, + pathToExtension: 'path', + environmentVariables: 'env vars', + parameters: 'params' + }, + language: { + name: 'name', + contents: [] + }, + newLang: false + }; + let tab = new AddEditLanguageTab(testContext.apiWrapper.object, parent, languageUpdateModel); + let content = tab.languageView?.updatedContent; + should.notEqual(content, undefined); + if (content) { + should.equal(content.extensionFileName, languageUpdateModel.content.extensionFileName); + should.equal(content.pathToExtension, languageUpdateModel.content.pathToExtension); + should.equal(content.environmentVariables, languageUpdateModel.content.environmentVariables); + should.equal(content.parameters, languageUpdateModel.content.parameters); + } + }); + + it('Should raise save event if save button clicked ', async function (): Promise { + let testContext = createContext(); + let parent = new ParentDialog(testContext.apiWrapper.object); + let languageUpdateModel: LanguageUpdateModel = { + content: parent.createNewContent(), + language: parent.createNewLanguage(), + newLang: true + }; + let tab = new AddEditLanguageTab(testContext.apiWrapper.object, parent, languageUpdateModel); + should.notEqual(tab.saveButton, undefined); + let updateCalled = false; + let promise = new Promise(resolve => { + parent.onUpdate(() => { + updateCalled = true; + resolve(); + }); + }); + + testContext.onClick.fire(); + parent.onUpdatedLanguage(languageUpdateModel); + await promise; + should.equal(updateCalled, true); + should.notEqual(tab.updatedData, undefined); + }); +}); 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 new file mode 100644 index 0000000000..f4007d5d17 --- /dev/null +++ b/extensions/machine-learning-services/src/test/views/externalLanguages/languageController.test.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 should from 'should'; +import 'mocha'; +import * as TypeMoq from 'typemoq'; +import { createContext } from './utils'; +import { LanguageController } from '../../../externalLanguage/languageController'; +import * as mssql from '../../../../../mssql/src/mssql'; + +describe('External Languages Controller', () => { + it('Should open dialog for manage languages successfully ', async function (): Promise { + let testContext = createContext(); + let controller = new LanguageController(testContext.apiWrapper.object, '', testContext.dialogModel.object); + let dialog = await controller.manageLanguages(); + testContext.apiWrapper.verify(x => x.openDialog(TypeMoq.It.isAny()), TypeMoq.Times.once()); + should.notEqual(dialog, undefined); + }); + + it('Should list languages successfully ', async function (): Promise { + let testContext = createContext(); + let languages: mssql.ExternalLanguage[] = [{ + name: '', + contents: [{ + extensionFileName: '', + isLocalFile: true, + pathToExtension: '', + }] + }]; + + testContext.dialogModel.setup( x=> x.getLanguageList()).returns(() => Promise.resolve(languages)); + let controller = new LanguageController(testContext.apiWrapper.object, '', testContext.dialogModel.object); + let dialog = await controller.manageLanguages(); + let actual = await dialog.listLanguages(); + should.deepEqual(actual, languages); + }); + + it('Should update languages successfully ', async function (): Promise { + let testContext = createContext(); + let language: mssql.ExternalLanguage = { + name: '', + contents: [{ + extensionFileName: '', + isLocalFile: true, + pathToExtension: '', + }] + }; + + testContext.dialogModel.setup( x=> x.updateLanguage(language)).returns(() => Promise.resolve()); + let controller = new LanguageController(testContext.apiWrapper.object, '', testContext.dialogModel.object); + let dialog = await controller.manageLanguages(); + await dialog.updateLanguage({ + language: language, + content: language.contents[0], + newLang: false + }); + testContext.dialogModel.verify(x => x.updateLanguage(TypeMoq.It.isAny()), TypeMoq.Times.once()); + }); + + it('Should delete language successfully ', async function (): Promise { + let testContext = createContext(); + let language: mssql.ExternalLanguage = { + name: '', + contents: [{ + extensionFileName: '', + isLocalFile: true, + pathToExtension: '', + }] + }; + + testContext.dialogModel.setup( x=> x.deleteLanguage(language.name)).returns(() => Promise.resolve()); + let controller = new LanguageController(testContext.apiWrapper.object, '', testContext.dialogModel.object); + let dialog = await controller.manageLanguages(); + await dialog.deleteLanguage({ + language: language, + content: language.contents[0], + newLang: false + }); + testContext.dialogModel.verify(x => x.deleteLanguage(TypeMoq.It.isAny()), TypeMoq.Times.once()); + }); + + it('Should open edit dialog for edit language', async function (): Promise { + let testContext = createContext(); + let language: mssql.ExternalLanguage = { + name: '', + contents: [{ + extensionFileName: '', + isLocalFile: true, + pathToExtension: '', + }] + }; + let controller = new LanguageController(testContext.apiWrapper.object, '', testContext.dialogModel.object); + let dialog = await controller.manageLanguages(); + dialog.onEditLanguage({ + language: language, + content: language.contents[0], + newLang: false + }); + testContext.apiWrapper.verify(x => x.openDialog(TypeMoq.It.isAny()), TypeMoq.Times.exactly(2)); + should.notEqual(dialog, undefined); + }); +}); diff --git a/extensions/machine-learning-services/src/test/views/externalLanguages/languageEditDialog.test.ts b/extensions/machine-learning-services/src/test/views/externalLanguages/languageEditDialog.test.ts new file mode 100644 index 0000000000..cd8f4e51a0 --- /dev/null +++ b/extensions/machine-learning-services/src/test/views/externalLanguages/languageEditDialog.test.ts @@ -0,0 +1,50 @@ +/*--------------------------------------------------------------------------------------------- + * 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, ParentDialog } from './utils'; +import { LanguageEditDialog } from '../../../views/externalLanguages/languageEditDialog'; +import { LanguageUpdateModel } from '../../../views/externalLanguages/languageViewBase'; + +describe('Edit External Languages Dialog', () => { + it('Should open dialog successfully ', async function (): Promise { + let testContext = createContext(); + let parent = new ParentDialog(testContext.apiWrapper.object); + let languageUpdateModel: LanguageUpdateModel = { + content: parent.createNewContent(), + language: parent.createNewLanguage(), + newLang: true + }; + let dialog = new LanguageEditDialog(testContext.apiWrapper.object, parent, languageUpdateModel); + dialog.showDialog(); + should.notEqual(dialog.addNewLanguageTab, undefined); + }); + + it('Should raise save event if save button clicked ', async function (): Promise { + let testContext = createContext(); + let parent = new ParentDialog(testContext.apiWrapper.object); + let languageUpdateModel: LanguageUpdateModel = { + content: parent.createNewContent(), + language: parent.createNewLanguage(), + newLang: true + }; + let dialog = new LanguageEditDialog(testContext.apiWrapper.object, parent, languageUpdateModel); + dialog.showDialog(); + + let updateCalled = false; + let promise = new Promise(resolve => { + parent.onUpdate(() => { + updateCalled = true; + parent.onUpdatedLanguage(languageUpdateModel); + resolve(); + }); + }); + + dialog.onSave(); + await promise; + should.equal(updateCalled, true); + }); +}); diff --git a/extensions/machine-learning-services/src/test/views/externalLanguages/languagesDialog.test.ts b/extensions/machine-learning-services/src/test/views/externalLanguages/languagesDialog.test.ts new file mode 100644 index 0000000000..535f78e6a6 --- /dev/null +++ b/extensions/machine-learning-services/src/test/views/externalLanguages/languagesDialog.test.ts @@ -0,0 +1,19 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { LanguagesDialog } from '../../../views/externalLanguages/languagesDialog'; + +describe('External Languages Dialog', () => { + it('Should open dialog successfully ', async function (): Promise { + let testContext = createContext(); + let dialog = new LanguagesDialog(testContext.apiWrapper.object, ''); + dialog.showDialog(); + should.notEqual(dialog.addNewLanguageTab, undefined); + should.notEqual(dialog.currentLanguagesTab, undefined); + }); +}); diff --git a/extensions/machine-learning-services/src/test/views/externalLanguages/languagesDialogModel.test.ts b/extensions/machine-learning-services/src/test/views/externalLanguages/languagesDialogModel.test.ts new file mode 100644 index 0000000000..b41b44f898 --- /dev/null +++ b/extensions/machine-learning-services/src/test/views/externalLanguages/languagesDialogModel.test.ts @@ -0,0 +1,61 @@ +/*--------------------------------------------------------------------------------------------- + * 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 * as mssql from '../../../../../mssql/src/mssql'; +import { LanguageService } from '../../../externalLanguage/languageService'; + +describe('External Languages Dialog Model', () => { + it('Should list languages successfully ', async function (): Promise { + let testContext = createContext(); + let languages: mssql.ExternalLanguage[] = [{ + name: '', + contents: [{ + extensionFileName: '', + isLocalFile: true, + pathToExtension: '', + }] + }]; + testContext.languageExtensionService.listLanguages = () => {return Promise.resolve(languages);}; + let model = new LanguageService(testContext.apiWrapper.object, testContext.languageExtensionService); + await model.load(); + let actual = await model.getLanguageList(); + should.deepEqual(actual, languages); + }); + + it('Should update language successfully ', async function (): Promise { + let testContext = createContext(); + let language: mssql.ExternalLanguage = { + name: '', + contents: [{ + extensionFileName: '', + isLocalFile: true, + pathToExtension: '', + }] + }; + + let model = new LanguageService(testContext.apiWrapper.object, testContext.languageExtensionService); + await model.load(); + await should(model.updateLanguage(language)).resolved(); + }); + + it('Should delete language successfully ', async function (): Promise { + let testContext = createContext(); + let language: mssql.ExternalLanguage = { + name: '', + contents: [{ + extensionFileName: '', + isLocalFile: true, + pathToExtension: '', + }] + }; + + let model = new LanguageService(testContext.apiWrapper.object, testContext.languageExtensionService); + await model.load(); + await should(model.deleteLanguage(language.name)).resolved(); + }); +}); diff --git a/extensions/machine-learning-services/src/test/views/externalLanguages/utils.ts b/extensions/machine-learning-services/src/test/views/externalLanguages/utils.ts new file mode 100644 index 0000000000..a7e9f990b3 --- /dev/null +++ b/extensions/machine-learning-services/src/test/views/externalLanguages/utils.ts @@ -0,0 +1,236 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { LanguageViewBase } from '../../../views/externalLanguages/languageViewBase'; +import * as mssql from '../../../../../mssql/src/mssql'; +import { LanguageService } from '../../../externalLanguage/languageService'; + +export interface TestContext { + apiWrapper: TypeMoq.IMock; + view: azdata.ModelView; + languageExtensionService: mssql.ILanguageExtensionService; + onClick: vscode.EventEmitter; + dialogModel: TypeMoq.IMock; +} + +export class ParentDialog extends LanguageViewBase { + public reset(): Promise { + return Promise.resolve(); + } + constructor( + apiWrapper: ApiWrapper) { + super(apiWrapper, ''); + } +} + +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! + } + }; + 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 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'); }); + + let languageExtensionService: mssql.ILanguageExtensionService = { + listLanguages: () => { return Promise.resolve([]); }, + deleteLanguage: () => { return Promise.resolve(); }, + updateLanguage: () => { return Promise.resolve(); } + }; + + + return { + apiWrapper: apiWrapper, + view: view, + languageExtensionService: languageExtensionService, + onClick: onClick, + dialogModel: TypeMoq.Mock.ofType(LanguageService) + }; +} diff --git a/extensions/machine-learning-services/src/views/externalLanguages/addEditLanguageTab.ts b/extensions/machine-learning-services/src/views/externalLanguages/addEditLanguageTab.ts new file mode 100644 index 0000000000..844d46934f --- /dev/null +++ b/extensions/machine-learning-services/src/views/externalLanguages/addEditLanguageTab.ts @@ -0,0 +1,89 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { LanguageViewBase, LanguageUpdateModel } from './languageViewBase'; +import { LanguageContentView } from './languageContentView'; +import { ApiWrapper } from '../../common/apiWrapper'; + +export class AddEditLanguageTab extends LanguageViewBase { + private _dialogTab: azdata.window.DialogTab; + public languageName: azdata.TextComponent | undefined; + private _editMode: boolean = false; + public saveButton: azdata.ButtonComponent | undefined; + public languageView: LanguageContentView | undefined; + + constructor( + apiWrapper: ApiWrapper, + parent: LanguageViewBase, + private _languageUpdateModel: LanguageUpdateModel) { + super(apiWrapper, parent.root, parent); + this._editMode = !this._languageUpdateModel.newLang; + this._dialogTab = apiWrapper.createTab(constants.extLangNewLanguageTabTitle); + this._dialogTab.registerContent(async view => { + let language = this._languageUpdateModel.language; + let content = this._languageUpdateModel.content; + this.languageName = view.modelBuilder.inputBox().withProperties({ + value: language.name, + width: '150px', + enabled: !this._editMode + }).withValidation(component => component.value !== '').component(); + + let formBuilder = view.modelBuilder.formContainer(); + formBuilder.addFormItem({ + component: this.languageName, + title: constants.extLangLanguageName, + required: true + }); + + this.languageView = new LanguageContentView(this._apiWrapper, this, view.modelBuilder, formBuilder, content); + + if (!this._editMode) { + this.saveButton = view.modelBuilder.button().withProperties({ + label: constants.extLangInstallButtonText, + width: '100px' + }).component(); + this.saveButton.onDidClick(async () => { + try { + await this.updateLanguage(this.updatedData); + } catch (err) { + this.showErrorMessage(constants.extLangInstallFailedError, err); + } + }); + + formBuilder.addFormItem({ + component: this.saveButton, + title: '' + }); + } + + await view.initializeModel(formBuilder.component()); + await this.reset(); + }); + } + + public get updatedData(): LanguageUpdateModel { + return { + language: { + name: this.languageName?.value || '', + contents: this._languageUpdateModel.language.contents + }, + content: this.languageView?.updatedContent || this._languageUpdateModel.content, + newLang: this._languageUpdateModel.newLang + }; + } + + public get tab(): azdata.window.DialogTab { + return this._dialogTab; + } + + public async reset(): Promise { + if (this.languageName) { + this.languageName.value = this._languageUpdateModel.language.name; + } + this.languageView?.reset(); + } +} diff --git a/extensions/machine-learning-services/src/views/externalLanguages/currentLanguagesTab.ts b/extensions/machine-learning-services/src/views/externalLanguages/currentLanguagesTab.ts new file mode 100644 index 0000000000..aa3f2472eb --- /dev/null +++ b/extensions/machine-learning-services/src/views/externalLanguages/currentLanguagesTab.ts @@ -0,0 +1,85 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { LanguageViewBase } from './languageViewBase'; +import { LanguagesTable } from './languagesTable'; +import { ApiWrapper } from '../../common/apiWrapper'; + +export class CurrentLanguagesTab extends LanguageViewBase { + + private _installedLangsTab: azdata.window.DialogTab; + + private _locationComponent: azdata.TextComponent | undefined; + private _installLanguagesTable: azdata.DeclarativeTableComponent | undefined; + private _languageTable: LanguagesTable | undefined; + private _loader: azdata.LoadingComponent | undefined; + + constructor(apiWrapper: ApiWrapper, parent: LanguageViewBase) { + super(apiWrapper, parent.root, parent); + this._installedLangsTab = this._apiWrapper.createTab(constants.extLangInstallTabTitle); + + this._installedLangsTab.registerContent(async view => { + + // TODO: only supporting single location for now. We should add a drop down for multi locations mode + // + let locationTitle = await this.getLocationTitle(); + this._locationComponent = view.modelBuilder.text().withProperties({ + value: locationTitle + }).component(); + + this._languageTable = new LanguagesTable(apiWrapper, view.modelBuilder, this); + this._installLanguagesTable = this._languageTable.table; + + let formModel = view.modelBuilder.formContainer() + .withFormItems([{ + component: this._locationComponent, + title: constants.extLangTarget + }, { + component: this._installLanguagesTable, + title: '' + }]).component(); + + this._loader = view.modelBuilder.loadingComponent() + .withItem(formModel) + .withProperties({ + loading: true + }).component(); + + await view.initializeModel(this._loader); + await this.reset(); + }); + } + + public get tab(): azdata.window.DialogTab { + return this._installedLangsTab; + } + + 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 async reset(): Promise { + await this.onLoading(); + + try { + await this._languageTable?.reset(); + } catch (err) { + this.showErrorMessage(constants.getErrorMessage(err)); + } finally { + await this.onLoaded(); + } + } +} diff --git a/extensions/machine-learning-services/src/views/externalLanguages/fileBrowserDialog.ts b/extensions/machine-learning-services/src/views/externalLanguages/fileBrowserDialog.ts new file mode 100644 index 0000000000..119270e24c --- /dev/null +++ b/extensions/machine-learning-services/src/views/externalLanguages/fileBrowserDialog.ts @@ -0,0 +1,69 @@ +/*--------------------------------------------------------------------------------------------- + * 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 constants from '../../common/constants'; +import { ApiWrapper } from '../../common/apiWrapper'; + +export class FileBrowserDialog { + + private _selectedPathTextBox: azdata.InputBoxComponent | undefined; + private _fileBrowserDialog: azdata.window.Dialog | undefined; + private _fileBrowserTree: azdata.FileBrowserTreeComponent | undefined; + + private _onPathSelected: vscode.EventEmitter = new vscode.EventEmitter(); + public readonly onPathSelected: vscode.Event = this._onPathSelected.event; + + constructor(private _apiWrapper: ApiWrapper, private ownerUri: string) { + } + + /** + * Opens a dialog to browse server files and folders. + */ + public showDialog(): void { + let fileBrowserTitle = ''; + this._fileBrowserDialog = this._apiWrapper.createModelViewDialog(fileBrowserTitle); + let fileBrowserTab = this._apiWrapper.createTab(constants.extLangFileBrowserTabTitle); + this._fileBrowserDialog.content = [fileBrowserTab]; + fileBrowserTab.registerContent(async (view) => { + this._fileBrowserTree = view.modelBuilder.fileBrowserTree() + .withProperties({ ownerUri: this.ownerUri, width: 420, height: 700 }) + .component(); + this._selectedPathTextBox = view.modelBuilder.inputBox() + .withProperties({ inputType: 'text' }) + .component(); + this._fileBrowserTree.onDidChange((args) => { + if (this._selectedPathTextBox) { + this._selectedPathTextBox.value = args.fullPath; + } + }); + + let fileBrowserContainer = view.modelBuilder.formContainer() + .withFormItems([{ + component: this._fileBrowserTree, + title: '' + }, { + component: this._selectedPathTextBox, + title: constants.extLangSelectedPath + } + ]).component(); + view.initializeModel(fileBrowserContainer); + }); + this._fileBrowserDialog.okButton.onClick(() => { + if (this._selectedPathTextBox) { + let selectedPath = this._selectedPathTextBox.value || ''; + this._onPathSelected.fire(selectedPath); + } + }); + + this._fileBrowserDialog.cancelButton.onClick(() => { + this._onPathSelected.fire(''); + }); + this._fileBrowserDialog.okButton.label = constants.extLangOkButtonText; + this._fileBrowserDialog.cancelButton.label = constants.extLangCancelButtonText; + this._apiWrapper.openDialog(this._fileBrowserDialog); + } +} diff --git a/extensions/machine-learning-services/src/views/externalLanguages/languageContentView.ts b/extensions/machine-learning-services/src/views/externalLanguages/languageContentView.ts new file mode 100644 index 0000000000..3be3d04c04 --- /dev/null +++ b/extensions/machine-learning-services/src/views/externalLanguages/languageContentView.ts @@ -0,0 +1,156 @@ +/*--------------------------------------------------------------------------------------------- + * 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 mssql from '../../../../mssql/src/mssql'; +import { LanguageViewBase } from './languageViewBase'; +import * as constants from '../../common/constants'; +import { ApiWrapper } from '../../common/apiWrapper'; + +export class LanguageContentView extends LanguageViewBase { + + private _serverPath: azdata.RadioButtonComponent; + private _localPath: azdata.RadioButtonComponent; + public extensionFile: azdata.TextComponent; + public extensionFileName: azdata.TextComponent; + public envVariables: azdata.TextComponent; + public parameters: azdata.TextComponent; + private _isLocalPath: boolean = true; + + /** + * + */ + constructor( + apiWrapper: ApiWrapper, + parent: LanguageViewBase, + private _modelBuilder: azdata.ModelBuilder, + private _formBuilder: azdata.FormBuilder, + private _languageContent: mssql.ExternalLanguageContent | undefined, + ) { + super(apiWrapper, parent.root, parent); + this._localPath = this._modelBuilder.radioButton() + .withProperties({ + value: 'local', + name: 'extensionLocation', + label: constants.extLangLocal, + checked: true + }).component(); + + this._serverPath = this._modelBuilder.radioButton() + .withProperties({ + value: 'server', + name: 'extensionLocation', + label: this.getServerTitle(), + }).component(); + + this._localPath.onDidClick(() => { + this._isLocalPath = true; + }); + this._serverPath.onDidClick(() => { + this._isLocalPath = false; + }); + + + let flexRadioButtonsModel = this._modelBuilder.flexContainer() + .withLayout({ + flexFlow: 'row', + justifyContent: 'space-between' + //width: parent.componentMaxLength + }).withItems([ + this._localPath, this._serverPath] + ).component(); + + this.extensionFile = this._modelBuilder.inputBox().withProperties({ + value: '', + width: parent.componentMaxLength - parent.browseButtonMaxLength - parent.spaceBetweenComponentsLength + }).component(); + let fileBrowser = this._modelBuilder.button().withProperties({ + label: '...', + width: parent.browseButtonMaxLength, + CSSStyles: { + 'text-align': 'end' + } + }).component(); + + let flexFilePathModel = this._modelBuilder.flexContainer() + .withLayout({ + flexFlow: 'row', + justifyContent: 'space-between' + }).withItems([ + this.extensionFile, fileBrowser] + ).component(); + this.filePathSelected(args => { + this.extensionFile.value = args.filePath; + }); + fileBrowser.onDidClick(async () => { + this.onOpenFileBrowser({ filePath: '', target: this._isLocalPath ? constants.localhost : this.connectionUrl }); + }); + + this.extensionFileName = this._modelBuilder.inputBox().withProperties({ + value: '', + width: parent.componentMaxLength + }).component(); + + this.envVariables = this._modelBuilder.inputBox().withProperties({ + value: '', + width: parent.componentMaxLength + }).component(); + this.parameters = this._modelBuilder.inputBox().withProperties({ + value: '', + width: parent.componentMaxLength + }).component(); + + this.load(); + + this._formBuilder.addFormItems([{ + component: flexRadioButtonsModel, + title: constants.extLangExtensionFileLocation + }, { + component: flexFilePathModel, + title: constants.extLangExtensionFilePath, + required: true + }, { + component: this.extensionFileName, + title: constants.extLangExtensionFileName, + required: true + }, { + component: this.envVariables, + title: constants.extLangEnvVariables + }, { + component: this.parameters, + title: constants.extLangParameters + }]); + } + + private load() { + if (this._languageContent) { + this._isLocalPath = this._languageContent.isLocalFile; + this._localPath.checked = this._isLocalPath; + this._serverPath.checked = !this._isLocalPath; + this.extensionFile.value = this._languageContent.pathToExtension; + this.extensionFileName.value = this._languageContent.extensionFileName; + this.envVariables.value = this._languageContent.environmentVariables; + this.parameters.value = this._languageContent.parameters; + } + } + + public async reset(): Promise { + this._isLocalPath = true; + this._localPath.checked = this._isLocalPath; + this._serverPath.checked = !this._isLocalPath; + this.load(); + } + + public get updatedContent(): mssql.ExternalLanguageContent { + return { + pathToExtension: this.extensionFile.value || '', + extensionFileName: this.extensionFileName.value || '', + parameters: this.parameters.value || '', + environmentVariables: this.envVariables.value || '', + isLocalFile: this._isLocalPath || false, + platform: this._languageContent?.platform + }; + } +} diff --git a/extensions/machine-learning-services/src/views/externalLanguages/languageEditDialog.ts b/extensions/machine-learning-services/src/views/externalLanguages/languageEditDialog.ts new file mode 100644 index 0000000000..0fd9425cc3 --- /dev/null +++ b/extensions/machine-learning-services/src/views/externalLanguages/languageEditDialog.ts @@ -0,0 +1,59 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as constants from '../../common/constants'; +import { AddEditLanguageTab } from './addEditLanguageTab'; +import { LanguageViewBase, LanguageUpdateModel } from './languageViewBase'; +import { ApiWrapper } from '../../common/apiWrapper'; + +export class LanguageEditDialog extends LanguageViewBase { + + public addNewLanguageTab: AddEditLanguageTab | undefined; + + constructor( + apiWrapper: ApiWrapper, + parent: LanguageViewBase, + private _languageUpdateModel: LanguageUpdateModel) { + super(apiWrapper, parent.root, parent); + } + + /** + * Opens a dialog to edit a language or a content of a language + */ + public showDialog(): void { + this._dialog = this._apiWrapper.createModelViewDialog(constants.extLangDialogTitle); + + this.addNewLanguageTab = new AddEditLanguageTab(this._apiWrapper, this, this._languageUpdateModel); + + this._dialog.cancelButton.label = constants.extLangCancelButtonText; + this._dialog.okButton.label = constants.extLangSaveButtonText; + + this.dialog?.registerCloseValidator(async (): Promise => { + return await this.onSave(); + }); + + this._dialog.content = [this.addNewLanguageTab.tab]; + this._apiWrapper.openDialog(this._dialog); + } + + public async onSave(): Promise { + if (this.addNewLanguageTab) { + try { + await this.updateLanguage(this.addNewLanguageTab.updatedData); + return true; + } catch (err) { + this.showErrorMessage(constants.extLangUpdateFailedError, err); + return false; + } + } + return false; + } + /** + * Resets the tabs for given provider Id + */ + public async reset(): Promise { + await this.addNewLanguageTab?.reset(); + } +} diff --git a/extensions/machine-learning-services/src/views/externalLanguages/languageViewBase.ts b/extensions/machine-learning-services/src/views/externalLanguages/languageViewBase.ts new file mode 100644 index 0000000000..8180c140e6 --- /dev/null +++ b/extensions/machine-learning-services/src/views/externalLanguages/languageViewBase.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 constants from '../../common/constants'; +import { ApiWrapper } from '../../common/apiWrapper'; +import * as mssql from '../../../../mssql/src/mssql'; +import * as path from 'path'; + +export interface LanguageUpdateModel { + language: mssql.ExternalLanguage, + content: mssql.ExternalLanguageContent, + newLang: boolean +} + +export interface FileBrowseEventArgs { + filePath: string, + target: string +} + +export abstract class LanguageViewBase { + protected _dialog: azdata.window.Dialog | undefined; + public connection: azdata.connection.ConnectionProfile | undefined; + public connectionUrl: string = ''; + + // Events + // + protected _onEdit: vscode.EventEmitter = new vscode.EventEmitter(); + public readonly onEdit: vscode.Event = this._onEdit.event; + + protected _onUpdate: vscode.EventEmitter = new vscode.EventEmitter(); + public readonly onUpdate: vscode.Event = this._onUpdate.event; + + protected _onDelete: vscode.EventEmitter = new vscode.EventEmitter(); + public readonly onDelete: vscode.Event = this._onDelete.event; + + protected _fileBrowser: vscode.EventEmitter = new vscode.EventEmitter(); + public readonly fileBrowser: vscode.Event = this._fileBrowser.event; + + protected _filePathSelected: vscode.EventEmitter = new vscode.EventEmitter(); + public readonly filePathSelected: vscode.Event = this._filePathSelected.event; + + protected _onUpdated: vscode.EventEmitter = new vscode.EventEmitter(); + public readonly onUpdated: vscode.Event = this._onUpdated.event; + + protected _onList: vscode.EventEmitter = new vscode.EventEmitter(); + public readonly onList: vscode.Event = this._onList.event; + + protected _onListLoaded: vscode.EventEmitter = new vscode.EventEmitter(); + public readonly onListLoaded: vscode.Event = this._onListLoaded.event; + + protected _onFailed: vscode.EventEmitter = new vscode.EventEmitter(); + public readonly onFailed: vscode.Event = this._onFailed.event; + + public componentMaxLength = 350; + public browseButtonMaxLength = 20; + public spaceBetweenComponentsLength = 10; + + constructor(protected _apiWrapper: ApiWrapper, protected _root?: string, protected _parent?: LanguageViewBase, ) { + if (this._parent) { + if (!this._root) { + this._root = this._parent.root; + } + this.connection = this._parent.connection; + this.connectionUrl = this._parent.connectionUrl; + } + this.registerEvents(); + } + + private registerEvents() { + if (this._parent) { + this._dialog = this._parent.dialog; + this.fileBrowser(url => { + this._parent?.onOpenFileBrowser(url); + }); + this.onUpdate(model => { + this._parent?.onUpdateLanguage(model); + }); + this.onEdit(model => { + this._parent?.onEditLanguage(model); + }); + this.onDelete(model => { + this._parent?.onDeleteLanguage(model); + }); + this.onList(() => { + this._parent?.onListLanguages(); + }); + this._parent.filePathSelected(x => { + this.onFilePathSelected(x); + }); + this._parent.onUpdated(x => { + this.onUpdatedLanguage(x); + }); + this._parent.onFailed(x => { + this.onActionFailed(x); + }); + this._parent.onListLoaded(x => { + this.onListLanguageLoaded(x); + }); + } + } + public async getLocationTitle(): Promise { + let connection = await this.getCurrentConnection(); + if (connection) { + return `${connection.serverName} ${connection.databaseName ? connection.databaseName : constants.extLangLocal}`; + } + 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(); + } + + public updateLanguage(updateModel: LanguageUpdateModel): Promise { + return new Promise((resolve, reject) => { + this.onUpdateLanguage(updateModel); + this.onUpdated(() => { + resolve(); + }); + this.onFailed(err => { + reject(err); + }); + }); + } + + public deleteLanguage(model: LanguageUpdateModel): Promise { + return new Promise((resolve, reject) => { + this.onDeleteLanguage(model); + this.onUpdated(() => { + resolve(); + }); + this.onFailed(err => { + reject(err); + }); + }); + } + + public listLanguages(): Promise { + return new Promise((resolve, reject) => { + this.onListLanguages(); + this.onListLoaded(list => { + resolve(list); + }); + this.onFailed(err => { + reject(err); + }); + }); + } + + /** + * Dialog model instance + */ + public get dialog(): azdata.window.Dialog | undefined { + return this._dialog; + } + + public set dialog(value: azdata.window.Dialog | undefined) { + this._dialog = 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); + } + + public onUpdateLanguage(model: LanguageUpdateModel): void { + this._onUpdate.fire(model); + } + + public onUpdatedLanguage(model: LanguageUpdateModel): void { + this._onUpdated.fire(model); + } + + public onActionFailed(error: any): void { + this._onFailed.fire(error); + } + + public onListLanguageLoaded(list: mssql.ExternalLanguage[]): void { + this._onListLoaded.fire(list); + } + + public onEditLanguage(model: LanguageUpdateModel): void { + this._onEdit.fire(model); + } + + public onDeleteLanguage(model: LanguageUpdateModel): void { + this._onDelete.fire(model); + } + + public onListLanguages(): void { + this._onList.fire(); + } + + public onOpenFileBrowser(fileBrowseArgs: FileBrowseEventArgs): void { + this._fileBrowser.fire(fileBrowseArgs); + } + + public onFilePathSelected(fileBrowseArgs: FileBrowseEventArgs): void { + this._filePathSelected.fire(fileBrowseArgs); + } + + private showMessage(message: string, level: azdata.window.MessageLevel): void { + if (this._dialog) { + this._dialog.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 reset(): Promise; + + public createNewContent(): mssql.ExternalLanguageContent { + return { + extensionFileName: '', + isLocalFile: true, + pathToExtension: '', + }; + } + + public createNewLanguage(): mssql.ExternalLanguage { + return { + name: '', + contents: [] + }; + } +} diff --git a/extensions/machine-learning-services/src/views/externalLanguages/languagesDialog.ts b/extensions/machine-learning-services/src/views/externalLanguages/languagesDialog.ts new file mode 100644 index 0000000000..0680b03a08 --- /dev/null +++ b/extensions/machine-learning-services/src/views/externalLanguages/languagesDialog.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 { CurrentLanguagesTab } from './currentLanguagesTab'; +import { AddEditLanguageTab } from './addEditLanguageTab'; +import { LanguageViewBase } from './languageViewBase'; +import * as constants from '../../common/constants'; +import { ApiWrapper } from '../../common/apiWrapper'; + +export class LanguagesDialog extends LanguageViewBase { + + public currentLanguagesTab: CurrentLanguagesTab | undefined; + public addNewLanguageTab: AddEditLanguageTab | undefined; + + constructor( + apiWrapper: ApiWrapper, + root: string) { + super(apiWrapper, root); + } + + /** + * Opens a dialog to manage packages used by notebooks. + */ + public showDialog(): void { + this.dialog = this._apiWrapper.createModelViewDialog(constants.extLangDialogTitle); + + this.currentLanguagesTab = new CurrentLanguagesTab(this._apiWrapper, this); + + let languageUpdateModel = { + language: this.createNewLanguage(), + content: this.createNewContent(), + newLang: true + }; + this.addNewLanguageTab = new AddEditLanguageTab(this._apiWrapper, this, languageUpdateModel); + + this.dialog.okButton.hidden = true; + this.dialog.cancelButton.label = constants.extLangDoneButtonText; + this.dialog.content = [this.currentLanguagesTab.tab, this.addNewLanguageTab.tab]; + + this.dialog.registerCloseValidator(() => { + return false; // Blocks Enter key from closing dialog. + }); + + this._apiWrapper.openDialog(this.dialog); + } + + /** + * Resets the tabs for given provider Id + */ + public async reset(): Promise { + await this.currentLanguagesTab?.reset(); + await this.addNewLanguageTab?.reset(); + } +} diff --git a/extensions/machine-learning-services/src/views/externalLanguages/languagesTable.ts b/extensions/machine-learning-services/src/views/externalLanguages/languagesTable.ts new file mode 100644 index 0000000000..838afc5542 --- /dev/null +++ b/extensions/machine-learning-services/src/views/externalLanguages/languagesTable.ts @@ -0,0 +1,165 @@ +/*--------------------------------------------------------------------------------------------- + * 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 * as mssql from '../../../../mssql/src/mssql'; +import { LanguageViewBase } from './languageViewBase'; +import { ApiWrapper } from '../../common/apiWrapper'; + +export class LanguagesTable extends LanguageViewBase { + + private _table: azdata.DeclarativeTableComponent; + + /** + * + */ + constructor(apiWrapper: ApiWrapper, private _modelBuilder: azdata.ModelBuilder, parent: LanguageViewBase) { + super(apiWrapper, parent.root, parent); + this._table = _modelBuilder.declarativeTable() + .withProperties( + { + columns: [ + { // Name + displayName: constants.extLangLanguageName, + ariaLabel: constants.extLangLanguageName, + valueType: azdata.DeclarativeDataType.string, + isReadOnly: true, + width: 100, + headerCssStyles: { + ...constants.cssStyles.tableHeader + }, + rowCssStyles: { + ...constants.cssStyles.tableRow + }, + }, + { // Platform + displayName: constants.extLangLanguagePlatform, + ariaLabel: constants.extLangLanguagePlatform, + valueType: azdata.DeclarativeDataType.string, + isReadOnly: true, + width: 150, + headerCssStyles: { + ...constants.cssStyles.tableHeader + }, + rowCssStyles: { + ...constants.cssStyles.tableRow + }, + }, + { // Created Date + displayName: constants.extLangLanguageCreatedDate, + ariaLabel: constants.extLangLanguageCreatedDate, + 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 + }, + }, + { // Action + displayName: '', + valueType: azdata.DeclarativeDataType.component, + isReadOnly: true, + width: 50, + headerCssStyles: { + ...constants.cssStyles.tableHeader + }, + rowCssStyles: { + ...constants.cssStyles.tableRow + }, + } + ], + data: [], + ariaLabel: constants.mlsConfigTitle + }) + .component(); + } + + public get table(): azdata.DeclarativeTableComponent { + return this._table; + } + + public async loadData(): Promise { + let languages: mssql.ExternalLanguage[] | undefined; + + languages = await this.listLanguages(); + let tableData: any[][] = []; + + if (languages) { + + languages.forEach(language => { + if (!language.contents || language.contents.length === 0) { + language.contents.push(this.createNewContent()); + } + + tableData = tableData.concat(language.contents.map(content => this.createTableRow(language, content))); + }); + } + + this._table.data = tableData; + } + + private createTableRow(language: mssql.ExternalLanguage, content: mssql.ExternalLanguageContent): any[] { + if (this._modelBuilder) { + let dropLanguageButton = this._modelBuilder.button().withProperties({ + label: '', + title: constants.deleteTitle, + iconPath: { + dark: this.asAbsolutePath('images/dark/delete_inverse.svg'), + light: this.asAbsolutePath('images/light/delete.svg') + }, + width: 15, + height: 15 + }).component(); + dropLanguageButton.onDidClick(async () => { + await this.deleteLanguage({ + language: language, + content: content, + newLang: false + }); + }); + + 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(() => { + this.onEditLanguage({ + language: language, + content: content, + newLang: false + }); + }); + return [language.name, content.platform, language.createdDate, dropLanguageButton, editLanguageButton]; + } + + return []; + } + + public async reset(): Promise { + await this.loadData(); + } +} diff --git a/extensions/mssql/src/constants.ts b/extensions/mssql/src/constants.ts index 36493f6abe..f56111d4fb 100644 --- a/extensions/mssql/src/constants.ts +++ b/extensions/mssql/src/constants.ts @@ -36,6 +36,7 @@ export const ObjectExplorerService = 'objectexplorer'; export const CmsService = 'cmsService'; export const DacFxService = 'dacfxService'; export const SchemaCompareService = 'schemaCompareService'; +export const LanguageExtensionService = 'languageExtensionService'; export const objectExplorerPrefix: string = 'objectexplorer://'; export const ViewType = 'view'; diff --git a/extensions/mssql/src/contracts.ts b/extensions/mssql/src/contracts.ts index 1bac306d62..8326926317 100644 --- a/extensions/mssql/src/contracts.ts +++ b/extensions/mssql/src/contracts.ts @@ -536,6 +536,40 @@ export namespace RemoveServerGroupRequest { } // ------------------------------- ---------------------------------------- +// ------------------------------- ----------------------------- + +export interface LanguageExtensionRequestParam { + ownerUri: string; +} + +export interface ExternalLanguageRequestParam extends LanguageExtensionRequestParam { + languageName: string; +} + +export interface ExternalLanguageUpdateRequestParam extends LanguageExtensionRequestParam { + language: mssql.ExternalLanguage; +} + +export interface LanguageExtensionListResponseParam { + languages: mssql.ExternalLanguage[]; +} + + +export interface ExternalLanguageResponseParam { +} + +export namespace LanguageExtensibilityListRequest { + export const type = new RequestType('languageExtension/list'); +} + +export namespace LanguageExtensibilityDeleteRequest { + export const type = new RequestType('languageExtension/delete'); +} + +export namespace LanguageExtensibilityUpdateRequest { + export const type = new RequestType('languageExtension/update'); +} + // ------------------------------- ----------------------------- export interface SchemaCompareParams { operationId: string; diff --git a/extensions/mssql/src/languageExtension/languageExtensionService.ts b/extensions/mssql/src/languageExtension/languageExtensionService.ts new file mode 100644 index 0000000000..a1609d0c14 --- /dev/null +++ b/extensions/mssql/src/languageExtension/languageExtensionService.ts @@ -0,0 +1,71 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { AppContext } from '../appContext'; +import { SqlOpsDataClient, ISqlOpsFeature } from 'dataprotocol-client'; +import * as constants from '../constants'; +import * as mssql from '../mssql'; +import * as Utils from '../utils'; +import { ClientCapabilities } from 'vscode-languageclient'; +import * as contracts from '../contracts'; + +export class LanguageExtensionService implements mssql.ILanguageExtensionService { + + public static asFeature(context: AppContext): ISqlOpsFeature { + return class extends LanguageExtensionService { + constructor(client: SqlOpsDataClient) { + super(context, client); + } + + fillClientCapabilities(capabilities: ClientCapabilities): void { + Utils.ensure(capabilities, 'languageExtension')!.languageExtension = true; + } + + initialize(): void { + } + }; + } + + private constructor(context: AppContext, protected readonly client: SqlOpsDataClient) { + context.registerService(constants.LanguageExtensionService, this); + } + + public listLanguages(ownerUri: string): Thenable { + const params: contracts.LanguageExtensionRequestParam = { ownerUri: ownerUri }; + return this.client.sendRequest(contracts.LanguageExtensibilityListRequest.type, params).then( + r => { + return r.languages; + }, + e => { + this.client.logFailedRequest(contracts.LanguageExtensibilityListRequest.type, e); + return Promise.reject(e); + } + ); + } + + public updateLanguage(ownerUri: string, language: mssql.ExternalLanguage): Thenable { + const params: contracts.ExternalLanguageUpdateRequestParam = { ownerUri: ownerUri, language: language }; + return this.client.sendRequest(contracts.LanguageExtensibilityUpdateRequest.type, params).then( + () => { + }, + e => { + this.client.logFailedRequest(contracts.LanguageExtensibilityUpdateRequest.type, e); + return Promise.reject(e); + } + ); + } + + public deleteLanguage(ownerUri: string, languageName: string): Thenable { + const params: contracts.ExternalLanguageRequestParam = { ownerUri: ownerUri, languageName: languageName }; + return this.client.sendRequest(contracts.LanguageExtensibilityDeleteRequest.type, params).then( + () => { + }, + e => { + this.client.logFailedRequest(contracts.LanguageExtensibilityDeleteRequest.type, e); + return Promise.reject(e); + } + ); + } +} diff --git a/extensions/mssql/src/mssql.d.ts b/extensions/mssql/src/mssql.d.ts index b4787b7123..9664284ec1 100644 --- a/extensions/mssql/src/mssql.d.ts +++ b/extensions/mssql/src/mssql.d.ts @@ -38,6 +38,8 @@ export interface IExtension { readonly schemaCompare: ISchemaCompareService; + readonly languageExtension: ILanguageExtensionService; + readonly dacFx: IDacFxService; } @@ -379,6 +381,30 @@ export interface GenerateDeployPlan { //#endregion +//#region --- Language Extensibility +export interface ExternalLanguageContent { + pathToExtension: string; + extensionFileName: string; + platform?: string; + parameters?: string; + environmentVariables?: string; + isLocalFile: boolean; +} + +export interface ExternalLanguage { + name: string; + owner?: string; + contents: ExternalLanguageContent[]; + createdDate?: string; +} + +export interface ILanguageExtensionService { + listLanguages(ownerUri: string): Thenable; + deleteLanguage(ownerUri: string, languageName: string): Thenable; + updateLanguage(ownerUri: string, language: ExternalLanguage): Thenable; +} +//#endregion + //#region --- cms /** * diff --git a/extensions/mssql/src/mssqlApiFactory.ts b/extensions/mssql/src/mssqlApiFactory.ts index ddec871b83..0684823583 100644 --- a/extensions/mssql/src/mssqlApiFactory.ts +++ b/extensions/mssql/src/mssqlApiFactory.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { AppContext } from './appContext'; -import { IExtension, ICmsService, IDacFxService, ISchemaCompareService, MssqlObjectExplorerBrowser } from './mssql'; +import { IExtension, ICmsService, IDacFxService, ISchemaCompareService, MssqlObjectExplorerBrowser, ILanguageExtensionService } from './mssql'; import * as constants from './constants'; import { MssqlObjectExplorerNodeProvider } from './objectExplorerNodeProvider/objectExplorerNodeProvider'; import * as azdata from 'azdata'; @@ -20,6 +20,9 @@ export function createMssqlApi(context: AppContext): IExtension { get schemaCompare() { return context.getService(constants.SchemaCompareService); }, + get languageExtension() { + return context.getService(constants.LanguageExtensionService); + }, getMssqlObjectExplorerBrowser(): MssqlObjectExplorerBrowser { return { getNode: (explorerContext: azdata.ObjectExplorerContext) => { diff --git a/extensions/mssql/src/sqlToolsServer.ts b/extensions/mssql/src/sqlToolsServer.ts index 17e60c386d..c265c3717a 100644 --- a/extensions/mssql/src/sqlToolsServer.ts +++ b/extensions/mssql/src/sqlToolsServer.ts @@ -21,6 +21,7 @@ import { CmsService } from './cms/cmsService'; import { CompletionExtensionParams, CompletionExtLoadRequest } from './contracts'; import { promises as fs } from 'fs'; import * as nls from 'vscode-nls'; +import { LanguageExtensionService } from './languageExtension/languageExtensionService'; const localize = nls.loadMessageBundle(); const outputChannel = vscode.window.createOutputChannel(Constants.serviceName); @@ -152,6 +153,7 @@ function getClientOptions(context: AppContext): ClientOptions { AgentServicesFeature, SerializationFeature, SchemaCompareService.asFeature(context), + LanguageExtensionService.asFeature(context), DacFxService.asFeature(context), CmsService.asFeature(context) ],