Machine Learning Services Dashboard (#8796)

* Added a dashboard for Machine Learning Services Extension
This commit is contained in:
Leila Lali
2020-01-08 11:24:58 -08:00
committed by GitHub
parent 774151b6e9
commit 2d2376a2a6
15 changed files with 699 additions and 52 deletions

View File

@@ -10,7 +10,7 @@
"azdata": ">=1.13.0"
},
"activationEvents": [
"onCommand:ml.command.managePackages"
"*"
],
"license": "https://raw.githubusercontent.com/Microsoft/azuredatastudio/master/LICENSE.txt",
"icon": "images/ML_ExtensionIcon.png",
@@ -27,28 +27,66 @@
"contributes": {
"commands": [
{
"command": "ml.command.managePackages",
"title": "%ml.command.managePackages%"
"command": "mls.command.managePackages",
"title": "%mls.command.managePackages%"
},
{
"command": "mls.command.odbcdriver",
"title": "%mls.command.odbcdriver%"
},
{
"command": "mls.command.mlsdocs",
"title": "%mls.command.mlsdocs%"
}
],
"dashboard.tabs": [
{
"id": "mls-dashboard",
"description": "%description%",
"provider": "MSSQL",
"title": "%displayName%",
"when": "connectionProvider == 'MSSQL' && !mssql:iscloud",
"container": {
"grid-container": [
{
"name": "%title.configurations%",
"row": 0,
"col": 0,
"widget": {
"modelview": {
"id": "ml.tasks"
}
}
},
{
"name": "%title.tasks%",
"row": 0,
"col": 1,
"widget": {
"tasks-widget": [
"mls.command.managePackages",
"mls.command.odbcdriver",
"mls.command.mlsdocs"
]
}
}
]
}
}
]
},
"dependencies": {
"vscode-nls": "^4.0.0"
"vscode-nls": "^4.0.0",
"vscode-languageclient": "^5.3.0-next.1"
},
"devDependencies": {
"@types/mocha": "^5.2.5",
"@types/node": "^10.14.8",
"@types/uuid": "^3.4.5",
"mocha": "^5.2.0",
"mocha-junit-reporter": "^1.17.0",
"mocha-multi-reporters": "^1.1.7",
"should": "^13.2.1",
"typemoq": "^2.1.0",
"vscode": "1.1.26"
},
"__metadata": {
"id": "56",
"publisherDisplayName": "Microsoft",
"publisherId": "Microsoft"
}
}
}

View File

@@ -1,10 +1,11 @@
{
"displayName": "SQL Server Machine Learning Services",
"description": "SQL Server Machine Learning Services",
"mlServices.enable": "Enable Machine Learning Services",
"mlServices.disable": "Disable Machine Learning Services",
"title.tasks": "Getting Started",
"title.tasks": "Tasks",
"title.configurations": "Configurations",
"title.endpoints": "Endpoints",
"title.books": "Machine Learning Services Books",
"ml.command.managePackages": "Manage Packages in SQL Server"
"mls.command.managePackages": "Manage Packages in SQL Server",
"mls.command.odbcdriver": "Install ODBC Driver for SQL Server",
"mls.command.mlsdocs": "Machine Learning Services Documentation"
}

View File

@@ -34,6 +34,9 @@ export class ApiWrapper {
public executeCommand<T>(command: string, ...rest: any[]): Thenable<T | undefined> {
return vscode.commands.executeCommand(command, ...rest);
}
public registerTaskHandler(taskId: string, handler: (profile: azdata.IConnectionProfile) => void): void {
azdata.tasks.registerTask(taskId, handler);
}
public getUriForConnection(connectionId: string): Thenable<string> {
return azdata.connection.getUriForConnection(connectionId);
@@ -59,6 +62,10 @@ export class ApiWrapper {
azdata.tasks.startBackgroundOperation(operationInfo);
}
public openExternal(target: vscode.Uri): Thenable<boolean> {
return vscode.env.openExternal(target);
}
public getExtension(extensionId: string): vscode.Extension<any> | undefined {
return vscode.extensions.getExtension(extensionId);
}

View File

@@ -12,17 +12,65 @@ const localize = nls.loadMessageBundle();
export const winPlatform = 'win32';
export const pythonBundleVersion = '0.0.1';
export const managePackagesCommand = 'jupyter.cmd.managePackages';
export const mlManagePackagesCommand = 'ml.command.managePackages';
export const pythonLanguageName = 'Python';
export const rLanguageName = 'R';
export const mlEnableMlsCommand = 'mls.command.enableMls';
export const mlDisableMlsCommand = 'mls.command.disableMls';
export const extensionOutputChannel = 'Machine Learning Services';
export const notebookExtensionName = 'Microsoft.notebook';
// Tasks, commands
//
export const mlManagePackagesCommand = 'mls.command.managePackages';
export const mlOdbcDriverCommand = 'mls.command.odbcdriver';
export const mlsDocumentsCommand = 'mls.command.mlsdocs';
// Localized texts
//
export const managePackageCommandError = localize('ml.managePackages.error', "Either no connection is available or the server does not have external script enabled.");
export function installDependenciesError(err: string): string { return localize('ml.installDependencies.error', "Failed to install dependencies. Error: {0}", err); }
export const installDependenciesMsgTaskName = localize('ml.installDependencies.msgTaskName', "Installing Machine Learning extension dependencies");
export const installDependenciesPackages = localize('ml.installDependencies.packages', "Installing required packages ...");
export const installDependenciesPackagesAlreadyInstalled = localize('ml.installDependencies.packagesAlreadyInstalled', "Required packages are already installed.");
export function installDependenciesGetPackagesError(err: string): string { return localize('ml.installDependencies.getPackagesError', "Failed to get installed python packages. Error: {0}", err); }
export const packageManagerNoConnection = localize('ml.packageManager.NoConnection', "No connection selected");
export const notebookExtensionNotLoaded = localize('ml.notebookExtensionNotLoaded', "Notebook extension is not loaded");
export const managePackageCommandError = localize('mls.managePackages.error', "Either no connection is available or the server does not have external script enabled.");
export function installDependenciesError(err: string): string { return localize('mls.installDependencies.error', "Failed to install dependencies. Error: {0}", err); }
export const installDependenciesMsgTaskName = localize('mls.installDependencies.msgTaskName', "Installing Machine Learning extension dependencies");
export const installDependenciesPackages = localize('mls.installDependencies.packages', "Installing required packages ...");
export const installDependenciesPackagesAlreadyInstalled = localize('mls.installDependencies.packagesAlreadyInstalled', "Required packages are already installed.");
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 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");
export const mlsEnableButtonTitle = localize('mls.enableButtonTitle', "Enable");
export const mlsDisableButtonTitle = localize('mls.disableButtonTitle', "Disable");
export const mlsConfigTitle = localize('mls.configTitle', "Config");
export const mlsConfigStatus = localize('mls.configStatus', "Enabled");
export const mlsConfigAction = localize('mls.configAction', "Action");
export const mlsExternalExecuteScriptTitle = localize('mls.externalExecuteScriptTitle', "External Execute Script");
export const mlsPythonLanguageTitle = localize('mls.pythonLanguageTitle', "Python");
export const mlsRLanguageTitle = localize('mls.rLanguageTitle', "R");
// Links
//
export const mlsDocuments = 'https://docs.microsoft.com/sql/advanced-analytics/?view=sql-server-ver15';
export const odbcDriverWindowsDocuments = 'https://docs.microsoft.com/sql/connect/odbc/windows/microsoft-odbc-driver-for-sql-server-on-windows?view=sql-server-ver15';
export const odbcDriverLinuxDocuments = 'https://docs.microsoft.com/sql/connect/odbc/linux-mac/installing-the-microsoft-odbc-driver-for-sql-server?view=sql-server-ver15';
export const installMlsLinuxDocs = 'https://docs.microsoft.com/sql/linux/sql-server-linux-setup-machine-learning?toc=%2fsql%2fadvanced-analytics%2ftoc.json&view=sql-server-ver15';
export const installMlsWindowsDocs = 'https://docs.microsoft.com/sql/advanced-analytics/install/sql-machine-learning-services-windows-install?view=sql-server-ver15';
// CSS Styles
//
export namespace cssStyles {
export const title = { 'font-size': '14px', 'font-weight': '600' };
export const tableHeader = { 'text-align': 'left', 'font-weight': 'bold', 'text-transform': 'uppercase', 'font-size': '10px', 'user-select': 'text', 'border': 'none', 'background-color': '#FFFFFF' };
export const tableRow = { 'border-top': 'solid 1px #ccc', 'border-bottom': 'solid 1px #ccc', 'border-left': 'none', 'border-right': 'none' };
export const hyperlink = { 'user-select': 'text', 'color': '#0078d4', 'text-decoration': 'underline', 'cursor': 'pointer' };
export const text = { 'margin-block-start': '0px', 'margin-block-end': '0px' };
export const overflowEllipsisText = { ...text, 'overflow': 'hidden', 'text-overflow': 'ellipsis' };
export const nonSelectableText = { ...cssStyles.text, 'user-select': 'none' };
export const tabHeaderText = { 'margin-block-start': '2px', 'margin-block-end': '0px', 'user-select': 'none' };
export const selectedResourceHeaderTab = { 'font-weight': 'bold', 'color': '' };
export const unselectedResourceHeaderTab = { 'font-weight': '', 'color': '#0078d4' };
export const selectedTabDiv = { 'border-bottom': '2px solid #000' };
export const unselectedTabDiv = { 'border-bottom': '1px solid #ccc' };
export const lastUpdatedText = { ...text, 'color': '#595959' };
export const errorText = { ...text, 'color': 'red' };
}

View File

@@ -11,10 +11,13 @@ import * as childProcess from 'child_process';
const ExecScriptsTimeoutInSeconds = 600000;
export class ProcessService {
public timeout = ExecScriptsTimeoutInSeconds;
public async execScripts(exeFilePath: string, scripts: string[], outputChannel?: vscode.OutputChannel): Promise<void> {
return new Promise<void>((resolve, reject) => {
const scriptExecution = childProcess.spawn(exeFilePath);
let timer: NodeJS.Timeout;
let output: string;
scripts.forEach(script => {
scriptExecution.stdin.write(`${script}\n`);
@@ -34,19 +37,23 @@ export class ProcessService {
}
scriptExecution.on('exit', (code) => {
if (timer) {
clearTimeout(timer);
}
if (code === 0) {
resolve();
} else {
reject(`Process exited with code: ${code}. output: ${output}`);
}
});
setTimeout(() => {
timer = setTimeout(() => {
try {
scriptExecution.kill();
} catch (error) {
console.log(error);
}
}, ExecScriptsTimeoutInSeconds);
}, this.timeout);
});
}
@@ -56,7 +63,9 @@ export class ProcessService {
outputChannel.appendLine(` > ${cmd}`);
}
let child = childProcess.exec(cmd, (err, stdout) => {
let child = childProcess.exec(cmd, {
timeout: this.timeout
}, (err, stdout) => {
if (err) {
reject(err);
} else {

View File

@@ -8,6 +8,7 @@
import * as azdata from 'azdata';
import * as nbExtensionApis from '../typings/notebookServices';
import { ApiWrapper } from './apiWrapper';
import * as constants from '../common/constants';
const listPythonPackagesQuery = `
EXEC sp_execute_external_script
@@ -25,11 +26,11 @@ Declare @external_script_enabled bit
SELECT @external_script_enabled=config_value FROM @tablevar WHERE name = 'external scripts enabled'
SELECT @external_script_enabled`;
const checkPythonInstalledQuery = `
const checkLanguageInstalledQuery = `
SELECT is_installed
FROM sys.dm_db_external_language_stats s, sys.external_languages l
WHERE s.external_language_id = l.external_language_id AND language = 'Python'`;
WHERE s.external_language_id = l.external_language_id AND language = '#LANGUAGE#'`;
const modifyExternalScriptConfigQuery = `
@@ -86,7 +87,21 @@ export class QueryRunner {
* Returns true if python installed in the give SQL server instance
*/
public async isPythonInstalled(connection: azdata.connection.ConnectionProfile): Promise<boolean> {
let result = await this.runQuery(connection, checkPythonInstalledQuery);
return this.isLanguageInstalled(connection, constants.pythonLanguageName);
}
/**
* Returns true if R installed in the give SQL server instance
*/
public async isRInstalled(connection: azdata.connection.ConnectionProfile): Promise<boolean> {
return this.isLanguageInstalled(connection, constants.rLanguageName);
}
/**
* Returns true if language installed in the give SQL server instance
*/
private async isLanguageInstalled(connection: azdata.connection.ConnectionProfile, language: string): Promise<boolean> {
let result = await this.runQuery(connection, checkLanguageInstalledQuery.replace('#LANGUAGE#', language));
let isInstalled = false;
if (result && result.rows && result.rows.length > 0) {
isInstalled = result.rows[0][0].displayValue === '1';

View File

@@ -5,7 +5,7 @@
'use strict';
import * as uuid from 'uuid';
import * as UUID from 'vscode-languageclient/lib/utils/uuid';
import * as path from 'path';
import * as os from 'os';
import * as fs from 'fs';
@@ -15,7 +15,7 @@ import { promisify } from 'util';
export async function execCommandOnTempFile<T>(content: string, command: (filePath: string) => Promise<T>): Promise<T> {
let tempFilePath: string = '';
try {
tempFilePath = path.join(os.tmpdir(), `ads_ml_temp_${uuid.v4()}`);
tempFilePath = path.join(os.tmpdir(), `ads_ml_temp_${UUID.generateUuid()}`);
await fs.promises.writeFile(tempFilePath, content);
let result = await command(tempFilePath);
return result;
@@ -46,3 +46,7 @@ export function getPythonExePath(rootFolder: string): string {
constants.pythonBundleVersion,
process.platform === constants.winPlatform ? 'python.exe' : 'bin/python3');
}
export function isWindows(): boolean {
return process.platform === 'win32';
}

View File

@@ -6,6 +6,7 @@
'use strict';
import * as vscode from 'vscode';
import * as nbExtensionApis from '../typings/notebookServices';
import { PackageManager } from '../packageManagement/packageManager';
import * as constants from '../common/constants';
@@ -13,12 +14,13 @@ import { ApiWrapper } from '../common/apiWrapper';
import { QueryRunner } from '../common/queryRunner';
import { ProcessService } from '../common/processService';
import { Config } from '../common/config';
import { ServerConfigWidget } from '../widgets/serverConfigWidgets';
import { ServerConfigManager } from '../serverConfig/serverConfigManager';
/**
* The main controller class that initializes the extension
*/
export default class MainController implements vscode.Disposable {
private _outputChannel: vscode.OutputChannel;
private _rootPath = this._context.extensionPath;
private _config: Config;
@@ -28,7 +30,8 @@ export default class MainController implements vscode.Disposable {
private _apiWrapper: ApiWrapper,
private _queryRunner: QueryRunner,
private _processService: ProcessService,
private _packageManager?: PackageManager
private _packageManager?: PackageManager,
private _serverConfigManager?: ServerConfigManager
) {
this._outputChannel = this._apiWrapper.createOutputChannel(constants.extensionOutputChannel);
this._rootPath = this._context.extensionPath;
@@ -63,14 +66,28 @@ export default class MainController implements vscode.Disposable {
}
private async initialize(): Promise<void> {
this._outputChannel.show(true);
let nbApis = await this.getNotebookExtensionApis();
await this._config.load();
let tasks = new ServerConfigWidget(this._apiWrapper, this.serverConfigManager);
tasks.register();
let packageManager = this.getPackageManager(nbApis);
this._apiWrapper.registerCommand(constants.mlManagePackagesCommand, (async () => {
await packageManager.managePackages();
}));
this._apiWrapper.registerTaskHandler(constants.mlManagePackagesCommand, async () => {
await packageManager.managePackages();
});
this._apiWrapper.registerTaskHandler(constants.mlOdbcDriverCommand, async () => {
await this.serverConfigManager.openOdbcDriverDocuments();
});
this._apiWrapper.registerTaskHandler(constants.mlsDocumentsCommand, async () => {
await this.serverConfigManager.openDocuments();
});
try {
await packageManager.installDependencies();
} catch (err) {
@@ -90,10 +107,13 @@ export default class MainController implements vscode.Disposable {
}
/**
* Package manager instance
*/
public set packageManager(value: PackageManager) {
this._packageManager = value;
* Returns the server config manager instance
*/
public get serverConfigManager(): ServerConfigManager {
if (!this._serverConfigManager) {
this._serverConfigManager = new ServerConfigManager(this._apiWrapper, this._queryRunner);
}
return this._serverConfigManager;
}
/**

View File

@@ -0,0 +1,90 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import * as azdata from 'azdata';
import { QueryRunner } from '../common/queryRunner';
import * as constants from '../common/constants';
import { ApiWrapper } from '../common/apiWrapper';
import * as utils from '../common/utils';
export class ServerConfigManager {
/**
* Creates a new instance of ServerConfigManager
*/
constructor(
private _apiWrapper: ApiWrapper,
private _queryRunner: QueryRunner,
) {
}
/**
* Opens server config documents
*/
public async openDocuments(): Promise<boolean> {
return await this._apiWrapper.openExternal(vscode.Uri.parse(constants.mlsDocuments));
}
/**
* Opens ODBC driver documents
*/
public async openOdbcDriverDocuments(): Promise<boolean> {
if (utils.isWindows()) {
return await this._apiWrapper.openExternal(vscode.Uri.parse(constants.odbcDriverWindowsDocuments));
} else {
return await this._apiWrapper.openExternal(vscode.Uri.parse(constants.odbcDriverLinuxDocuments));
}
}
/**
* Opens install MLS documents
*/
public async openInstallDocuments(): Promise<boolean> {
if (utils.isWindows()) {
return await this._apiWrapper.openExternal(vscode.Uri.parse(constants.installMlsWindowsDocs));
} else {
return await this._apiWrapper.openExternal(vscode.Uri.parse(constants.installMlsLinuxDocs));
}
}
/**
* Returns true if mls is installed in the give SQL server instance
*/
public async isMachineLearningServiceEnabled(connection: azdata.connection.ConnectionProfile): Promise<boolean> {
return this._queryRunner.isMachineLearningServiceEnabled(connection);
}
/**
* Returns true if R installed in the give SQL server instance
*/
public async isRInstalled(connection: azdata.connection.ConnectionProfile): Promise<boolean> {
return this._queryRunner.isRInstalled(connection);
}
/**
* Returns true if python installed in the give SQL server instance
*/
public async isPythonInstalled(connection: azdata.connection.ConnectionProfile): Promise<boolean> {
return this._queryRunner.isPythonInstalled(connection);
}
/**
* Updates external script config
* @param connection SQL Connection
* @param enable if true external script will be enabled
*/
public async updateExternalScriptConfig(connection: azdata.connection.ConnectionProfile, enable: boolean): Promise<boolean> {
await this._queryRunner.updateExternalScriptConfig(connection, enable);
let current = await this._queryRunner.isMachineLearningServiceEnabled(connection);
if (current === enable) {
this._apiWrapper.showInfoMessage(enable ? constants.mlsEnabledMessage : constants.mlsDisabledMessage);
} else {
this._apiWrapper.showErrorMessage(constants.mlsConfigUpdateFailed);
}
return current;
}
}

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.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as vscode from 'vscode';
import { ProcessService } from '../../common/processService';
import * as utils from '../../common/utils';
import should = require('should');
interface TestContext {
outputChannel: vscode.OutputChannel;
}
function createContext(): TestContext {
return {
outputChannel: {
name: '',
append: () => { },
appendLine: () => { },
clear: () => { },
show: () => { },
hide: () => { },
dispose: () => { }
}
};
}
function execFolderListCommand(context: TestContext, service : ProcessService): Promise<void> {
if (utils.isWindows()) {
return service.execScripts('cmd', ['dir', '.'], context.outputChannel);
} else {
return service.execScripts('/bin/sh', ['-c', 'ls'], context.outputChannel);
}
}
function execFolderListBufferedCommand(context: TestContext, service : ProcessService): Promise<string> {
if (utils.isWindows()) {
return service.executeBufferedCommand('dir', context.outputChannel);
} else {
return service.executeBufferedCommand('ls', context.outputChannel);
}
}
describe('Process Service', () => {
it('Executing a valid script should return successfully', async function (): Promise<void> {
const context = createContext();
let service = new ProcessService();
await should(execFolderListCommand(context, service)).resolved();
});
it('execFolderListCommand should reject if command time out @UNSTABLE@', async function (): Promise<void> {
const context = createContext();
let service = new ProcessService();
service.timeout = 10;
await should(execFolderListCommand(context, service)).rejected();
});
it('executeBufferedCommand should resolve give valid script', async function (): Promise<void> {
const context = createContext();
let service = new ProcessService();
service.timeout = 2000;
await should(execFolderListBufferedCommand(context, service)).resolved();
});
});

View File

@@ -72,8 +72,7 @@ function createContext(): TestContext {
}
function createController(testContext: TestContext): MainController {
let controller = new MainController(testContext.context, testContext.apiWrapper.object, testContext.queryRunner.object, testContext.processService.object);
controller.packageManager = testContext.packageManager.object;
let controller = new MainController(testContext.context, testContext.apiWrapper.object, testContext.queryRunner.object, testContext.processService.object, testContext.packageManager.object);
return controller;
}

View File

@@ -0,0 +1,99 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as azdata from 'azdata';
import { QueryRunner } from '../../common/queryRunner';
import { ApiWrapper } from '../../common/apiWrapper';
import * as TypeMoq from 'typemoq';
import * as should from 'should';
import { ServerConfigManager } from '../../serverConfig/serverConfigManager';
interface TestContext {
apiWrapper: TypeMoq.IMock<ApiWrapper>;
queryRunner: TypeMoq.IMock<QueryRunner>;
}
function createContext(): TestContext {
return {
apiWrapper: TypeMoq.Mock.ofType(ApiWrapper),
queryRunner: TypeMoq.Mock.ofType(QueryRunner)
};
}
describe('Server Config Manager', () => {
it('openDocuments should open document in browser successfully', async function (): Promise<void> {
const context = createContext();
context.apiWrapper.setup(x => x.openExternal(TypeMoq.It.isAny())).returns(() => Promise.resolve(true));
let serverConfigManager = new ServerConfigManager(context.apiWrapper.object, context.queryRunner.object);
should.equal(await serverConfigManager.openDocuments(), true);
});
it('openOdbcDriverDocuments should open document in browser successfully', async function (): Promise<void> {
const context = createContext();
context.apiWrapper.setup(x => x.openExternal(TypeMoq.It.isAny())).returns(() => Promise.resolve(true));
let serverConfigManager = new ServerConfigManager(context.apiWrapper.object, context.queryRunner.object);
should.equal(await serverConfigManager.openOdbcDriverDocuments(), true);
});
it('openInstallDocuments should open document in browser successfully', async function (): Promise<void> {
const context = createContext();
context.apiWrapper.setup(x => x.openExternal(TypeMoq.It.isAny())).returns(() => Promise.resolve(true));
let serverConfigManager = new ServerConfigManager(context.apiWrapper.object, context.queryRunner.object);
should.equal(await serverConfigManager.openInstallDocuments(), true);
});
it('isMachineLearningServiceEnabled should return true if external script is enabled', async function (): Promise<void> {
const context = createContext();
context.queryRunner.setup(x => x.isMachineLearningServiceEnabled(TypeMoq.It.isAny())).returns(() => Promise.resolve(true));
let serverConfigManager = new ServerConfigManager(context.apiWrapper.object, context.queryRunner.object);
let connection = new azdata.connection.ConnectionProfile();
should.equal(await serverConfigManager.isMachineLearningServiceEnabled(connection), true);
});
it('isRInstalled should return true if R is installed', async function (): Promise<void> {
const context = createContext();
context.queryRunner.setup(x => x.isRInstalled(TypeMoq.It.isAny())).returns(() => Promise.resolve(true));
let serverConfigManager = new ServerConfigManager(context.apiWrapper.object, context.queryRunner.object);
let connection = new azdata.connection.ConnectionProfile();
should.equal(await serverConfigManager.isRInstalled(connection), true);
});
it('isPythonInstalled should return true if Python is installed', async function (): Promise<void> {
const context = createContext();
context.queryRunner.setup(x => x.isPythonInstalled(TypeMoq.It.isAny())).returns(() => Promise.resolve(true));
let serverConfigManager = new ServerConfigManager(context.apiWrapper.object, context.queryRunner.object);
let connection = new azdata.connection.ConnectionProfile();
should.equal(await serverConfigManager.isPythonInstalled(connection), true);
});
it('updateExternalScriptConfig should show info message if updated successfully', async function (): Promise<void> {
const context = createContext();
context.queryRunner.setup(x => x.updateExternalScriptConfig(TypeMoq.It.isAny(), true)).returns(() => Promise.resolve());
context.queryRunner.setup(x => x.isMachineLearningServiceEnabled(TypeMoq.It.isAny())).returns(() => Promise.resolve(true));
context.apiWrapper.setup(x => x.showInfoMessage(TypeMoq.It.isAny())).returns(() => Promise.resolve(''));
context.apiWrapper.setup(x => x.showErrorMessage(TypeMoq.It.isAny())).returns(() => Promise.resolve(''));
let serverConfigManager = new ServerConfigManager(context.apiWrapper.object, context.queryRunner.object);
let connection = new azdata.connection.ConnectionProfile();
await serverConfigManager.updateExternalScriptConfig(connection, true);
context.apiWrapper.verify(x => x.showInfoMessage(TypeMoq.It.isAny()), TypeMoq.Times.once());
});
it('updateExternalScriptConfig should show error message if did not updated successfully', async function (): Promise<void> {
const context = createContext();
context.queryRunner.setup(x => x.updateExternalScriptConfig(TypeMoq.It.isAny(), true)).returns(() => Promise.resolve());
context.queryRunner.setup(x => x.isMachineLearningServiceEnabled(TypeMoq.It.isAny())).returns(() => Promise.resolve(false));
context.apiWrapper.setup(x => x.showInfoMessage(TypeMoq.It.isAny())).returns(() => Promise.resolve(''));
context.apiWrapper.setup(x => x.showErrorMessage(TypeMoq.It.isAny())).returns(() => Promise.resolve(''));
let serverConfigManager = new ServerConfigManager(context.apiWrapper.object, context.queryRunner.object);
let connection = new azdata.connection.ConnectionProfile();
await serverConfigManager.updateExternalScriptConfig(connection, true);
context.apiWrapper.verify(x => x.showErrorMessage(TypeMoq.It.isAny()), TypeMoq.Times.once());
});
});

View File

@@ -0,0 +1,160 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata';
import { ApiWrapper } from '../common/apiWrapper';
import { ServerConfigManager } from '../serverConfig/serverConfigManager';
import * as constants from '../common/constants';
export class ConfigTable {
private _statusTable: azdata.DeclarativeTableComponent;
constructor(private _apiWrapper: ApiWrapper, private _serverConfigManager: ServerConfigManager, private _modelBuilder: azdata.ModelBuilder, private _loadingComponent: azdata.LoadingComponent) {
this._statusTable = this._modelBuilder.declarativeTable()
.withProperties<azdata.DeclarativeTableProperties>(
{
columns: [
{ // Config
displayName: constants.mlsConfigTitle,
valueType: azdata.DeclarativeDataType.component,
isReadOnly: true,
width: 175,
headerCssStyles: {
...constants.cssStyles.tableHeader
},
rowCssStyles: {
...constants.cssStyles.tableRow
},
},
{ // Status icon
displayName: constants.mlsConfigStatus,
ariaLabel: constants.mlsConfigStatus,
valueType: azdata.DeclarativeDataType.component,
isReadOnly: true,
width: 25,
headerCssStyles: {
...constants.cssStyles.tableHeader
},
rowCssStyles: {
...constants.cssStyles.tableRow
},
},
{ // Action
displayName: '',
valueType: azdata.DeclarativeDataType.component,
isReadOnly: true,
width: 150,
headerCssStyles: {
...constants.cssStyles.tableHeader
},
rowCssStyles: {
...constants.cssStyles.tableRow
},
}
],
data: [],
ariaLabel: constants.mlsConfigTitle
})
.component();
}
/**
* Returns the config table component
*/
public get component(): azdata.DeclarativeTableComponent {
return this._statusTable;
}
/**
* Refreshes the config table
*/
public async refresh(): Promise<void> {
this._loadingComponent.updateProperties({ loading: true });
let connection = await this.getCurrentConnection();
const externalScriptsConfig = await this.createTableRowComponents(constants.mlsExternalExecuteScriptTitle,
async () => {
return await this._serverConfigManager.isMachineLearningServiceEnabled(connection);
}, async (enable) => {
this._loadingComponent.updateProperties({ loading: true });
await this._serverConfigManager.updateExternalScriptConfig(connection, enable);
await this.refresh();
}
);
const pythonConfig = await this.createTableRowComponents(constants.mlsPythonLanguageTitle,
async () => {
return await this._serverConfigManager.isPythonInstalled(connection);
}, async () => {
await this._serverConfigManager.openInstallDocuments();
}
);
const rConfig = await this.createTableRowComponents(constants.mlsRLanguageTitle,
async () => {
return await this._serverConfigManager.isRInstalled(connection);
}, async () => {
await this._serverConfigManager.openInstallDocuments();
}
);
this._statusTable.data = [externalScriptsConfig, pythonConfig, rConfig];
this._loadingComponent.updateProperties({ loading: false });
}
private async getCurrentConnection(): Promise<azdata.connection.ConnectionProfile> {
return await this._apiWrapper.getCurrentConnection();
}
private async createTableRowComponents(configName: string, checkEnabledFunction: () => Promise<boolean>, updateFunction: (enable: boolean) => Promise<void>): Promise<azdata.Component[]> {
const isEnabled = await checkEnabledFunction();
const nameCell = this._modelBuilder.text()
.withProperties<azdata.TextComponentProperties>({
value: configName,
CSSStyles: { 'user-select': 'none', ...constants.cssStyles.text }
}).component();
const statusIconCell = this._modelBuilder.text()
.withProperties<azdata.TextComponentProperties>({
value: this.getConfigStatusIcon(isEnabled),
ariaRole: 'img',
title: this.getConfigStatusTest(isEnabled),
CSSStyles: { 'user-select': 'none', ...constants.cssStyles.text }
}).component();
const button = this._modelBuilder.button().withProperties<azdata.ButtonProperties>({
label: '',
title: ''
}).component();
button.label = this.getLabel(isEnabled);
button.onDidClick(async () => {
await updateFunction(!isEnabled);
const isEnabledNewValue = await checkEnabledFunction();
button.label = this.getLabel(isEnabledNewValue);
});
return [
nameCell,
statusIconCell,
button
];
}
private getConfigStatusIcon(enabled: boolean): string {
if (enabled) {
return '✔️';
} else {
return '❌';
}
}
private getConfigStatusTest(enabled: boolean): string {
if (enabled) {
return constants.mlsEnableButtonTitle;
} else {
return constants.mlsDisableButtonTitle;
}
}
private getLabel(isEnabled: boolean): string {
return isEnabled ? constants.mlsDisableButtonTitle : constants.mlsEnableButtonTitle;
}
}

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 { ApiWrapper } from '../common/apiWrapper';
import { ServerConfigManager } from '../serverConfig/serverConfigManager';
import { ConfigTable } from './configTable';
export class ServerConfigWidget {
constructor(private _apiWrapper: ApiWrapper, private _serverConfigManager: ServerConfigManager) {
}
/**
* Registers the widget and initializes the components
*/
public register(): void {
azdata.ui.registerModelViewProvider('ml.tasks', async (view) => {
const container = view.modelBuilder.flexContainer().withLayout({
flexFlow: 'column',
width: '100%',
height: '100%'
}).component();
const mainContainer = view.modelBuilder.flexContainer().withLayout({
flexFlow: 'column',
width: '270px',
height: '100%',
position: 'absolute'
}).component();
mainContainer.addItem(container, {
CSSStyles: {
'padding-top': '25px',
'padding-left': '5px'
}
});
let spinner = view.modelBuilder.loadingComponent()
.withItem(mainContainer)
.withProperties<azdata.LoadingComponentProperties>({ loading: true })
.component();
const configTable = new ConfigTable(this._apiWrapper, this._serverConfigManager, view.modelBuilder, spinner);
this.addRow(container, view, configTable.component);
await view.initializeModel(spinner);
await configTable.refresh();
});
}
private addRow(container: azdata.FlexContainer, view: azdata.ModelView, component: azdata.Component) {
const bookRow = view.modelBuilder.flexContainer().withLayout({
flexFlow: 'row',
justifyContent: 'space-between',
height: '100'
}).component();
bookRow.addItem(component, {
CSSStyles: {
'width': '100',
'hight': '100',
'padding-top': '10px',
'text-align': 'left'
}
});
container.addItems([bookRow]);
}
}

View File

@@ -7,23 +7,11 @@
resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-5.2.6.tgz#b8622d50557dd155e9f2f634b7d68fd38de5e94b"
integrity sha512-1axi39YdtBI7z957vdqXI4Ac25e7YihYQtJa+Clnxg1zTJEaIRbndt71O3sP4GAMgiAm0pY26/b9BrY4MR/PMw==
"@types/node@*":
version "12.12.17"
resolved "https://registry.yarnpkg.com/@types/node/-/node-12.12.17.tgz#191b71e7f4c325ee0fb23bc4a996477d92b8c39b"
integrity sha512-Is+l3mcHvs47sKy+afn2O1rV4ldZFU7W8101cNlOd+MRbjM4Onida8jSZnJdTe/0Pcf25g9BNIUsuugmE6puHA==
"@types/node@^10.14.8":
version "10.14.17"
resolved "https://registry.yarnpkg.com/@types/node/-/node-10.14.17.tgz#b96d4dd3e427382482848948041d3754d40fd5ce"
integrity sha512-p/sGgiPaathCfOtqu2fx5Mu1bcjuP8ALFg4xpGgNkcin7LwRyzUKniEHBKdcE1RPsenq5JVPIpMTJSygLboygQ==
"@types/uuid@^3.4.5":
version "3.4.6"
resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-3.4.6.tgz#d2c4c48eb85a757bf2927f75f939942d521e3016"
integrity sha512-cCdlC/1kGEZdEglzOieLDYBxHsvEOIg7kp/2FYyVR9Pxakq+Qf/inL3RKQ+PA8gOlI/NnL+fXmQH12nwcGzsHw==
dependencies:
"@types/node" "*"
ajv@^6.5.5:
version "6.10.0"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.10.0.tgz#90d0d54439da587cd7e843bfb7045f50bd22bdf1"
@@ -1230,6 +1218,11 @@ semver@^5.4.1:
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.0.tgz#790a7cf6fea5459bac96110b29b60412dc8ff96b"
integrity sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==
semver@^6.3.0:
version "6.3.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
should-equal@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/should-equal/-/should-equal-2.0.0.tgz#6072cf83047360867e68e98b09d71143d04ee0c3"
@@ -1593,6 +1586,32 @@ vinyl@^2.0.0, vinyl@^2.0.1, vinyl@^2.0.2:
remove-trailing-separator "^1.0.1"
replace-ext "^1.0.0"
vscode-jsonrpc@^5.0.0-next.5:
version "5.0.0-next.5"
resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-5.0.0-next.5.tgz#43284da590b86320e427c3256bbe6849d8c6a6bd"
integrity sha512-k9akfglxWgr0dtLNscq2uBq48XJwnhf4EaDxn05KQowRwR0DkNML0zeYqFRLtXZe6x5vpL5ppyu4o6GqL+23YQ==
vscode-languageclient@^5.3.0-next.1:
version "5.3.0-next.9"
resolved "https://registry.yarnpkg.com/vscode-languageclient/-/vscode-languageclient-5.3.0-next.9.tgz#34f58017647f15cd86015f7af45935dc750611f7"
integrity sha512-BFA3X1y2EI2CfsSBy0KG2Xr5BOYfd/97jTmD+doqL6oj+cY8S7AmRCOwb2f9Hbjq8GWL7YC+OJ0leZEUSPgP0A==
dependencies:
semver "^6.3.0"
vscode-languageserver-protocol "^3.15.0-next.8"
vscode-languageserver-protocol@^3.15.0-next.8:
version "3.15.0-next.14"
resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.15.0-next.14.tgz#e7eb337f1adb50b4a41c05d436ce03c8df1f4d14"
integrity sha512-xUwwno6Q6RFd2Z2EWV9D3dQlsKPnHyiZMNWq+EC7JJdp2WH1gRlD+KPX4UGRCnJK0WI5omqHV313IESPwRY5xA==
dependencies:
vscode-jsonrpc "^5.0.0-next.5"
vscode-languageserver-types "^3.15.0-next.9"
vscode-languageserver-types@^3.15.0-next.9:
version "3.15.0-next.9"
resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.15.0-next.9.tgz#957a9d1d5998a02edf62298fb7e37d9efcc6c157"
integrity sha512-Rl/8qJ6932nrHCdPn+9y0x08uLVQaSLRG+U4JzhyKpWU4eJbVaDRoAcz1Llj7CErJGbPr6kdBvShPy5fRfR+Uw==
vscode-nls@^4.0.0:
version "4.1.1"
resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-4.1.1.tgz#f9916b64e4947b20322defb1e676a495861f133c"