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)
],