Machine Learning Services Extension - External Languages (#9043)

* Added external language list, edit and delete UIs to Machine Learning extension
This commit is contained in:
Leila Lali
2020-02-10 08:58:46 -08:00
committed by GitHub
parent 8c61538a27
commit ac6a27b9c2
31 changed files with 1956 additions and 3 deletions

View File

@@ -0,0 +1,10 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0)">
<path d="M14 3H13V14.5C13 14.7083 12.9609 14.9036 12.8828 15.0859C12.8047 15.2682 12.6979 15.4271 12.5625 15.5625C12.4271 15.6979 12.2682 15.8047 12.0859 15.8828C11.9036 15.9609 11.7083 16 11.5 16H3.5C3.29167 16 3.09635 15.9609 2.91406 15.8828C2.73177 15.8047 2.57292 15.6979 2.4375 15.5625C2.30208 15.4271 2.19531 15.2682 2.11719 15.0859C2.03906 14.9036 2 14.7083 2 14.5V3H1V2H5V1C5 0.859375 5.02604 0.729167 5.07812 0.609375C5.13021 0.489583 5.20052 0.385417 5.28906 0.296875C5.38281 0.203125 5.48958 0.130208 5.60938 0.078125C5.72917 0.0260417 5.85938 0 6 0H9C9.14062 0 9.27083 0.0260417 9.39062 0.078125C9.51042 0.130208 9.61458 0.203125 9.70312 0.296875C9.79688 0.385417 9.86979 0.489583 9.92188 0.609375C9.97396 0.729167 10 0.859375 10 1V2H14V3ZM6 2H9V1H6V2ZM12 3H3V14.5C3 14.6354 3.04948 14.7526 3.14844 14.8516C3.2474 14.9505 3.36458 15 3.5 15H11.5C11.6354 15 11.7526 14.9505 11.8516 14.8516C11.9505 14.7526 12 14.6354 12 14.5V3ZM6 13H5V5H6V13ZM8 13H7V5H8V13ZM10 13H9V5H10V13Z" fill="white"/>
</g>
<defs>
<clipPath id="clip0">
<rect width="16" height="16" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><style type="text/css">.icon-canvas-transparent{opacity:0;fill:#2d2d30;} .icon-vs-out{fill:#2d2d30;} .icon-vs-bg{fill:#c5c5c5;}</style><path class="icon-canvas-transparent" d="M16 16h-16v-16h16v16z" id="canvas"/><path class="icon-vs-out" d="M16 4.28l-11.673 11.72h-4.327v-4.406l11.477-11.594h.308l4.215 4.237v.043z" id="outline" style="display: none;"/><path class="icon-vs-bg" d="M14.598 4.25l-1.688 1.75-3-3 1.688-1.75 3 3zm-5.688-.25l-7 7 3 3 7-7-3-3zm-7.91 8.09v2.91h2.91l-2.91-2.91z" id="iconBg"/></svg>

After

Width:  |  Height:  |  Size: 571 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2048 2048" width="24" height="24">
<path d="M1792 384h-128v1472q0 40-15 75t-41 61-61 41-75 15H448q-40 0-75-15t-61-41-41-61-15-75V384H128V256h512V128q0-27 10-50t27-40 41-28 50-10h384q27 0 50 10t40 27 28 41 10 50v128h512v128zM768 256h384V128H768v128zm768 128H384v1472q0 26 19 45t45 19h1024q26 0 45-19t19-45V384zM768 1664H640V640h128v1024zm256 0H896V640h128v1024zm256 0h-128V640h128v1024z" />
</svg>

After

Width:  |  Height:  |  Size: 451 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><style type="text/css">.icon-canvas-transparent{opacity:0;fill:#F6F6F6;} .icon-vs-out{fill:#F6F6F6;} .icon-vs-bg{fill:#424242;}</style><path class="icon-canvas-transparent" d="M16 16h-16v-16h16v16z" id="canvas"/><path class="icon-vs-out" d="M16 4.28l-11.673 11.72h-4.327v-4.406l11.477-11.594h.308l4.215 4.237v.043z" id="outline" style="display: none;"/><path class="icon-vs-bg" d="M14.598 4.25l-1.688 1.75-3-3 1.688-1.75 3 3zm-5.688-.25l-7 7 3 3 7-7-3-3zm-7.91 8.09v2.91h2.91l-2.91-2.91z" id="iconBg"/></svg>

After

Width:  |  Height:  |  Size: 571 B

View File

@@ -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"
]

View File

@@ -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",

View File

@@ -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);
}
}

View File

@@ -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
//

View File

@@ -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<mssql.ILanguageExtensionService> {
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<void> {
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
*/

View File

@@ -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<LanguagesDialog> {
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<T>(dialog: LanguageViewBase, func: (...args: any[]) => Promise<T>, ...args: any[]): Promise<T> {
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<string> {
return new Promise<string>((resolve) => {
let dialog = new FileBrowserDialog(this._apiWrapper, connectionUrl);
dialog.onPathSelected((selectedPath) => {
resolve(selectedPath);
});
dialog.showDialog();
});
}
public async getLocalFilePath(): Promise<string> {
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<void> {
await model.deleteLanguage(deleteModel.language.name);
}
public async listLanguages(model: LanguageService): Promise<mssql.ExternalLanguage[]> {
return await model.getLanguageList();
}
public async updateLanguage(model: LanguageService, updateModel: LanguageUpdateModel): Promise<void> {
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);
}
}

View File

@@ -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<mssql.ExternalLanguage[]> {
if (this.connectionUrl) {
return await this._languageExtensionService.listLanguages(this.connectionUrl);
}
return [];
}
public async deleteLanguage(languageName: string): Promise<void> {
if (this.connectionUrl) {
await this._languageExtensionService.deleteLanguage(this.connectionUrl, languageName);
}
}
public async updateLanguage(language: mssql.ExternalLanguage): Promise<void> {
if (this.connectionUrl) {
await this._languageExtensionService.updateLanguage(this.connectionUrl, language);
}
}
private async getCurrentConnectionUrl(): Promise<string> {
let connection = await this.getCurrentConnection();
if (connection) {
return await this._apiWrapper.getUriForConnection(connection.connectionId);
}
return '';
}
private async getCurrentConnection(): Promise<azdata.connection.ConnectionProfile> {
return await this._apiWrapper.getCurrentConnection();
}
}

View File

@@ -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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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);
});
});

View File

@@ -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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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);
});
});

View File

@@ -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<void> {
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<void> {
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);
});
});

View File

@@ -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<void> {
let testContext = createContext();
let dialog = new LanguagesDialog(testContext.apiWrapper.object, '');
dialog.showDialog();
should.notEqual(dialog.addNewLanguageTab, undefined);
should.notEqual(dialog.currentLanguagesTab, undefined);
});
});

View File

@@ -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<void> {
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<void> {
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<void> {
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();
});
});

View File

@@ -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<ApiWrapper>;
view: azdata.ModelView;
languageExtensionService: mssql.ILanguageExtensionService;
onClick: vscode.EventEmitter<any>;
dialogModel: TypeMoq.IMock<LanguageService>;
}
export class ParentDialog extends LanguageViewBase {
public reset(): Promise<void> {
return Promise.resolve();
}
constructor(
apiWrapper: ApiWrapper) {
super(apiWrapper, '');
}
}
export function createContext(): TestContext {
let onClick: vscode.EventEmitter<any> = new vscode.EventEmitter<any>();
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<azdata.ButtonComponent> = {
component: () => button,
withProperties: () => buttonBuilder,
withValidation: () => buttonBuilder
};
let radioButtonBuilder: azdata.ComponentBuilder<azdata.ButtonComponent> = {
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<azdata.DeclarativeTableComponent> = {
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<azdata.InputBoxComponent> = {
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<boolean>().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)
};
}

View File

@@ -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<void> {
if (this.languageName) {
this.languageName.value = this._languageUpdateModel.language.name;
}
this.languageView?.reset();
}
}

View File

@@ -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<void> {
if (this._loader) {
await this._loader.updateProperties({ loading: true });
}
}
private async onLoaded(): Promise<void> {
if (this._loader) {
await this._loader.updateProperties({ loading: false });
}
}
public async reset(): Promise<void> {
await this.onLoading();
try {
await this._languageTable?.reset();
} catch (err) {
this.showErrorMessage(constants.getErrorMessage(err));
} finally {
await this.onLoaded();
}
}
}

View File

@@ -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<string> = new vscode.EventEmitter<string>();
public readonly onPathSelected: vscode.Event<string> = 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);
}
}

View File

@@ -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<void> {
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
};
}
}

View File

@@ -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<boolean> => {
return await this.onSave();
});
this._dialog.content = [this.addNewLanguageTab.tab];
this._apiWrapper.openDialog(this._dialog);
}
public async onSave(): Promise<boolean> {
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<void> {
await this.addNewLanguageTab?.reset();
}
}

View File

@@ -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<LanguageUpdateModel> = new vscode.EventEmitter<LanguageUpdateModel>();
public readonly onEdit: vscode.Event<LanguageUpdateModel> = this._onEdit.event;
protected _onUpdate: vscode.EventEmitter<LanguageUpdateModel> = new vscode.EventEmitter<LanguageUpdateModel>();
public readonly onUpdate: vscode.Event<LanguageUpdateModel> = this._onUpdate.event;
protected _onDelete: vscode.EventEmitter<LanguageUpdateModel> = new vscode.EventEmitter<LanguageUpdateModel>();
public readonly onDelete: vscode.Event<LanguageUpdateModel> = this._onDelete.event;
protected _fileBrowser: vscode.EventEmitter<FileBrowseEventArgs> = new vscode.EventEmitter<FileBrowseEventArgs>();
public readonly fileBrowser: vscode.Event<FileBrowseEventArgs> = this._fileBrowser.event;
protected _filePathSelected: vscode.EventEmitter<FileBrowseEventArgs> = new vscode.EventEmitter<FileBrowseEventArgs>();
public readonly filePathSelected: vscode.Event<FileBrowseEventArgs> = this._filePathSelected.event;
protected _onUpdated: vscode.EventEmitter<LanguageUpdateModel> = new vscode.EventEmitter<LanguageUpdateModel>();
public readonly onUpdated: vscode.Event<LanguageUpdateModel> = this._onUpdated.event;
protected _onList: vscode.EventEmitter<void> = new vscode.EventEmitter<void>();
public readonly onList: vscode.Event<void> = this._onList.event;
protected _onListLoaded: vscode.EventEmitter<mssql.ExternalLanguage[]> = new vscode.EventEmitter<mssql.ExternalLanguage[]>();
public readonly onListLoaded: vscode.Event<mssql.ExternalLanguage[]> = this._onListLoaded.event;
protected _onFailed: vscode.EventEmitter<any> = new vscode.EventEmitter<any>();
public readonly onFailed: vscode.Event<any> = 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<string> {
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<string> {
let connection = await this.getCurrentConnection();
if (connection) {
return await this._apiWrapper.getUriForConnection(connection.connectionId);
}
return '';
}
private async getCurrentConnection(): Promise<azdata.connection.ConnectionProfile> {
return await this._apiWrapper.getCurrentConnection();
}
public async loadConnection(): Promise<void> {
this.connection = await this.getCurrentConnection();
this.connectionUrl = await this.getCurrentConnectionUrl();
}
public updateLanguage(updateModel: LanguageUpdateModel): Promise<void> {
return new Promise<void>((resolve, reject) => {
this.onUpdateLanguage(updateModel);
this.onUpdated(() => {
resolve();
});
this.onFailed(err => {
reject(err);
});
});
}
public deleteLanguage(model: LanguageUpdateModel): Promise<void> {
return new Promise<void>((resolve, reject) => {
this.onDeleteLanguage(model);
this.onUpdated(() => {
resolve();
});
this.onFailed(err => {
reject(err);
});
});
}
public listLanguages(): Promise<mssql.ExternalLanguage[]> {
return new Promise<mssql.ExternalLanguage[]>((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<void>;
public createNewContent(): mssql.ExternalLanguageContent {
return {
extensionFileName: '',
isLocalFile: true,
pathToExtension: '',
};
}
public createNewLanguage(): mssql.ExternalLanguage {
return {
name: '',
contents: []
};
}
}

View File

@@ -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<void> {
await this.currentLanguagesTab?.reset();
await this.addNewLanguageTab?.reset();
}
}

View File

@@ -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<azdata.DeclarativeTableProperties>(
{
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<void> {
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<void> {
await this.loadData();
}
}

View File

@@ -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';

View File

@@ -536,6 +536,40 @@ export namespace RemoveServerGroupRequest {
}
// ------------------------------- <CMS> ----------------------------------------
// ------------------------------- <Language Extensibility> -----------------------------
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<LanguageExtensionRequestParam, LanguageExtensionListResponseParam, void, void>('languageExtension/list');
}
export namespace LanguageExtensibilityDeleteRequest {
export const type = new RequestType<ExternalLanguageRequestParam, ExternalLanguageResponseParam, void, void>('languageExtension/delete');
}
export namespace LanguageExtensibilityUpdateRequest {
export const type = new RequestType<ExternalLanguageUpdateRequestParam, ExternalLanguageResponseParam, void, void>('languageExtension/update');
}
// ------------------------------- <Schema Compare> -----------------------------
export interface SchemaCompareParams {
operationId: string;

View File

@@ -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<mssql.ExternalLanguage[]> {
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<void> {
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<void> {
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);
}
);
}
}

View File

@@ -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<ExternalLanguage[]>;
deleteLanguage(ownerUri: string, languageName: string): Thenable<void>;
updateLanguage(ownerUri: string, language: ExternalLanguage): Thenable<void>;
}
//#endregion
//#region --- cms
/**
*

View File

@@ -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<ISchemaCompareService>(constants.SchemaCompareService);
},
get languageExtension() {
return context.getService<ILanguageExtensionService>(constants.LanguageExtensionService);
},
getMssqlObjectExplorerBrowser(): MssqlObjectExplorerBrowser {
return {
getNode: (explorerContext: azdata.ObjectExplorerContext) => {

View File

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