diff --git a/extensions/machine-learning/images/aiMlSqlServer.svg b/extensions/machine-learning/images/aiMlSqlServer.svg new file mode 100644 index 0000000000..94fbb8932b --- /dev/null +++ b/extensions/machine-learning/images/aiMlSqlServer.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/extensions/machine-learning/images/aml.svg b/extensions/machine-learning/images/aml.svg new file mode 100644 index 0000000000..d0a0056eba --- /dev/null +++ b/extensions/machine-learning/images/aml.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/extensions/machine-learning/images/fileUpload.svg b/extensions/machine-learning/images/fileUpload.svg new file mode 100644 index 0000000000..4d114b1957 --- /dev/null +++ b/extensions/machine-learning/images/fileUpload.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/extensions/machine-learning/images/imported.svg b/extensions/machine-learning/images/imported.svg new file mode 100644 index 0000000000..01246fddb6 --- /dev/null +++ b/extensions/machine-learning/images/imported.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/extensions/machine-learning/images/notebooksIntro.svg b/extensions/machine-learning/images/notebooksIntro.svg new file mode 100644 index 0000000000..7086362ab1 --- /dev/null +++ b/extensions/machine-learning/images/notebooksIntro.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/extensions/machine-learning/images/sqlServerMl.svg b/extensions/machine-learning/images/sqlServerMl.svg new file mode 100644 index 0000000000..1f1f586e01 --- /dev/null +++ b/extensions/machine-learning/images/sqlServerMl.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/extensions/machine-learning/images/video1.svg b/extensions/machine-learning/images/video1.svg deleted file mode 100644 index 01f4180c8b..0000000000 --- a/extensions/machine-learning/images/video1.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/extensions/machine-learning/images/video2.svg b/extensions/machine-learning/images/video2.svg deleted file mode 100644 index 618673834d..0000000000 --- a/extensions/machine-learning/images/video2.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/extensions/machine-learning/src/common/constants.ts b/extensions/machine-learning/src/common/constants.ts index a497b52b9f..a5a0690bac 100644 --- a/extensions/machine-learning/src/common/constants.ts +++ b/extensions/machine-learning/src/common/constants.ts @@ -124,6 +124,8 @@ export const extLangUpdateFailedError = localize('extLang.updateFailedError', "F export const modelUpdateFailedError = localize('models.modelUpdateFailedError', "Failed to update the model"); export const databaseName = localize('databaseName', "Models database"); export const tableName = localize('tableName', "Models table"); +export const existingTableName = localize('existingTableName', "Existing table"); +export const newTableName = localize('newTableName', "New table"); export const modelName = localize('models.name', "Name"); export const modelFileName = localize('models.fileName', "File"); export const modelDescription = localize('models.description', "Description"); @@ -211,20 +213,20 @@ export const sqlMlDocTitle = localize('sqlMlDocTitle', "SQL machine learning doc export const sqlMlDocDesc = localize('sqlMlDocDesc', "Learn how to use machine learning in SQL Server and SQL on Azure, to run Python and R scripts on relational data."); export const sqlMlsDocTitle = localize('sqlMlsDocTitle', "SQL Server Machine Learning Services (Python and R)"); export const sqlMlsDocDesc = localize('sqlMlsDocDesc', "Get started with Machine Learning Services on SQL Server and how to install it on Windows and Linux."); -export const sqlMlsAzureDocTitle = localize('sqlMlsAzureDocTitle', "Machine Learning Services in Azure SQL Managed Instance (preview)"); -export const sqlMlsAzureDocDesc = localize('sqlMlsAzureDocDesc', "Get started with Machine Learning Services in Azure SQL Managed Instances."); +export const sqlMlsMIDocTitle = localize('sqlMlsMIDocTitle', "Machine Learning Services in Azure SQL Managed Instance (preview)"); +export const sqlMlsMIDocDesc = localize('sqlMlsMIDocDesc', "Get started with Machine Learning Services in Azure SQL Managed Instances."); export const mlsInstallOdbcDocTitle = localize('mlsInstallObdcDocTitle', "Install the Microsoft ODBC driver for SQL Server"); export const mlsInstallOdbcDocDesc = localize('mlsInstallOdbcDocDesc', "This document explains how to install the Microsoft ODBC Driver for SQL Server."); +export const onnxOnEdgeOdbcDocTitle = localize('onnxOnEdgeOdbcDocTitle', "Machine learning and AI with ONNX in SQL Database Edge Preview"); +export const onnxOnEdgeOdbcDocDesc = localize('onnxOnEdgeOdbcDocDesc', "Get started with machine learning in Azure SQL Database Edge"); // 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 mlDocLink = 'https://docs.microsoft.com/sql/machine-learning/'; -export const mlsDocLink = 'https://docs.microsoft.com/sql/machine-learning/what-is-sql-server-machine-learning'; -export const mlsAzureDocLink = 'https://docs.microsoft.com/azure/sql-database/sql-database-managed-instance-machine-learning-services-overview'; -export const installMlsWindowsDocs = 'https://docs.microsoft.com/sql/advanced-analytics/install/sql-machine-learning-services-windows-install?view=sql-server-ver15'; +export const odbcDriverDocuments = 'https://go.microsoft.com/fwlink/?linkid=2129818'; +export const mlDocLink = 'https://go.microsoft.com/fwlink/?linkid=2128671'; +export const mlsDocLink = 'https://go.microsoft.com/fwlink/?linkid=2128672'; +export const mlsAzureDocLink = 'https://go.microsoft.com/fwlink/?linkid=2128673'; +export const onnxOnEdgeDocs = 'https://go.microsoft.com/fwlink/?linkid=2128882'; // CSS Styles // diff --git a/extensions/machine-learning/src/packageManagement/packageManagementService.ts b/extensions/machine-learning/src/packageManagement/packageManagementService.ts index b817b354e8..d22d604412 100644 --- a/extensions/machine-learning/src/packageManagement/packageManagementService.ts +++ b/extensions/machine-learning/src/packageManagement/packageManagementService.ts @@ -3,7 +3,6 @@ * 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'; @@ -22,13 +21,6 @@ export class PackageManagementService { ) { } - /** - * Opens server config documents - */ - public async openDocuments(): Promise { - return await this._apiWrapper.openExternal(vscode.Uri.parse(constants.mlsDocuments)); - } - /** * Returns true if mls is installed in the give SQL server instance */ diff --git a/extensions/machine-learning/src/prediction/predictService.ts b/extensions/machine-learning/src/prediction/predictService.ts index db7682c5c5..7172bcf336 100644 --- a/extensions/machine-learning/src/prediction/predictService.ts +++ b/extensions/machine-learning/src/prediction/predictService.ts @@ -177,7 +177,7 @@ AS ( ) SELECT ${this.getPredictColumnNames(columns, 'predict_input')}, ${this.getPredictInputColumnNames(outputColumns, 'p')} -FROM PREDICT(MODEL = @model, DATA = predict_input) +FROM PREDICT(MODEL = @model, DATA = predict_input, runtime=onnx) WITH ( ${this.getOutputParameters(outputColumns)} ) AS p @@ -198,7 +198,7 @@ AS ( ) SELECT ${this.getPredictColumnNames(columns, 'predict_input')}, ${this.getOutputColumnNames(outputColumns, 'p')} -FROM PREDICT(MODEL = ${modelBytes}, DATA = predict_input) +FROM PREDICT(MODEL = ${modelBytes}, DATA = predict_input, runtime=onnx) WITH ( ${this.getOutputParameters(outputColumns)} ) AS p diff --git a/extensions/machine-learning/src/test/packageManagement/packageManagementService.test.ts b/extensions/machine-learning/src/test/packageManagement/packageManagementService.test.ts index 86adf320a7..fb25c3f5db 100644 --- a/extensions/machine-learning/src/test/packageManagement/packageManagementService.test.ts +++ b/extensions/machine-learning/src/test/packageManagement/packageManagementService.test.ts @@ -24,13 +24,6 @@ function createContext(): TestContext { } describe('Package Management Service', () => { - it('openDocuments should open document in browser successfully', async function (): Promise { - const context = createContext(); - context.apiWrapper.setup(x => x.openExternal(TypeMoq.It.isAny())).returns(() => Promise.resolve(true)); - let serverConfigManager = new PackageManagementService(context.apiWrapper.object, context.queryRunner.object); - should.equal(await serverConfigManager.openDocuments(), true); - }); - it('isMachineLearningServiceEnabled should return true if external script is enabled', async function (): Promise { const context = createContext(); context.queryRunner.setup(x => x.isMachineLearningServiceEnabled(TypeMoq.It.isAny())).returns(() => Promise.resolve(true)); diff --git a/extensions/machine-learning/src/test/views/utils.ts b/extensions/machine-learning/src/test/views/utils.ts index 2ac5441332..b43f1ee7cd 100644 --- a/extensions/machine-learning/src/test/views/utils.ts +++ b/extensions/machine-learning/src/test/views/utils.ts @@ -115,6 +115,10 @@ export function createViewContext(): ViewTestContext { onCardSelectedChanged: onClick.event }); + let group: () => azdata.GroupContainer = () => Object.assign({}, componentBase, container, { + collapsed: false, + }); + let declarativeTableBuilder: azdata.ComponentBuilder = { component: () => declarativeTable(), withProperties: () => declarativeTableBuilder, @@ -172,6 +176,15 @@ export function createViewContext(): ViewTestContext { withProperties: () => cardBuilder, withValidation: () => cardBuilder }; + let groupBuilder: azdata.GroupBuilder = { + component: () => { + return group(); + }, + withProperties: () => groupBuilder, + withValidation: () => groupBuilder, + withItems: () => groupBuilder, + withLayout: () => groupBuilder + }; let imageBuilder: azdata.ComponentBuilder = { component: () => { @@ -223,7 +236,7 @@ export function createViewContext(): ViewTestContext { dashboardWidget: undefined!, dashboardWebview: undefined!, formContainer: () => formBuilder, - groupContainer: undefined!, + groupContainer: () => groupBuilder, toolbarContainer: undefined!, loadingComponent: () => loadingBuilder, fileBrowserTree: undefined!, diff --git a/extensions/machine-learning/src/views/models/localModelsComponent.ts b/extensions/machine-learning/src/views/models/localModelsComponent.ts index 1306d72938..1c5aefad59 100644 --- a/extensions/machine-learning/src/views/models/localModelsComponent.ts +++ b/extensions/machine-learning/src/views/models/localModelsComponent.ts @@ -62,9 +62,13 @@ export class LocalModelsComponent extends ModelViewBase implements IDataComponen .withLayout({ flexFlow: 'row', justifyContent: 'space-between', - width: this.componentMaxLength + 200 + width: this.componentMaxLength }).withItems([ - this._localPath, this._localBrowse] + this._localPath, this._localBrowse], { + CSSStyles: { + 'padding-right': '5px' + } + } ).component(); this._form = modelBuilder.formContainer().withFormItems([{ diff --git a/extensions/machine-learning/src/views/models/modelSourcesComponent.ts b/extensions/machine-learning/src/views/models/modelSourcesComponent.ts index 9fa58db490..d644f9ecc4 100644 --- a/extensions/machine-learning/src/views/models/modelSourcesComponent.ts +++ b/extensions/machine-learning/src/views/models/modelSourcesComponent.ts @@ -40,6 +40,7 @@ export class ModelSourcesComponent extends ModelViewBase implements IDataCompone label: constants.localModelSource, selected: this._sourceType === ModelSourceType.Local, cardType: azdata.CardType.VerticalButton, + iconPath: { light: this.asAbsolutePath('images/fileUpload.svg'), dark: this.asAbsolutePath('images/fileUpload.svg') }, width: 50 }).component(); this._amlModel = modelBuilder.card() @@ -49,6 +50,7 @@ export class ModelSourcesComponent extends ModelViewBase implements IDataCompone label: constants.azureModelSource, selected: this._sourceType === ModelSourceType.Azure, cardType: azdata.CardType.VerticalButton, + iconPath: { light: this.asAbsolutePath('images/aml.svg'), dark: this.asAbsolutePath('images/aml.svg') }, width: 50 }).component(); @@ -59,6 +61,7 @@ export class ModelSourcesComponent extends ModelViewBase implements IDataCompone label: constants.registeredModelsSource, selected: this._sourceType === ModelSourceType.RegisteredModels, cardType: azdata.CardType.VerticalButton, + iconPath: { light: this.asAbsolutePath('images/imported.svg'), dark: this.asAbsolutePath('images/imported.svg') }, width: 50 }).component(); diff --git a/extensions/machine-learning/src/views/models/tableSelectionComponent.ts b/extensions/machine-learning/src/views/models/tableSelectionComponent.ts index 06eb5614cd..6c7b853566 100644 --- a/extensions/machine-learning/src/views/models/tableSelectionComponent.ts +++ b/extensions/machine-learning/src/views/models/tableSelectionComponent.ts @@ -29,6 +29,7 @@ export class TableSelectionComponent extends ModelViewBase implements IDataCompo private _dbTableComponent: azdata.FlexContainer | undefined; private tableMaxLength = this.componentMaxLength * 2 + 70; private _onSelectedChanged: vscode.EventEmitter = new vscode.EventEmitter(); + private _existingTablesSelected: boolean = true; public readonly onSelectedChanged: vscode.Event = this._onSelectedChanged.event; /** @@ -45,24 +46,66 @@ export class TableSelectionComponent extends ModelViewBase implements IDataCompo public registerComponent(modelBuilder: azdata.ModelBuilder, databaseTitle: string, tableTitle: string): azdata.Component { this._databases = modelBuilder.dropDown().withProperties({ width: this.componentMaxLength, - editable: this._settings.editable, - fireOnTextChange: this._settings.editable }).component(); this._tables = modelBuilder.dropDown().withProperties({ - width: this.componentMaxLength, - editable: this._settings.editable, - fireOnTextChange: this._settings.editable + width: this.componentMaxLength - 10, }).component(); this._databases.onValueChanged(async () => { await this.onDatabaseSelected(); }); + const existingTableButton = modelBuilder.radioButton().withProperties({ + name: 'tableName', + value: 'existing', + label: 'Existing table', + checked: true + }).component(); + const newTableButton = modelBuilder.radioButton().withProperties({ + name: 'tableName', + value: 'new', + label: 'New table', + checked: false + }).component(); + const newTableName = modelBuilder.inputBox().withProperties({ + width: this.componentMaxLength - 10, + enabled: false + }).component(); + const group = modelBuilder.groupContainer().withItems([ + existingTableButton, + this._tables, + newTableButton, + newTableName + ], { + CSSStyles: { + 'padding-top': '5px' + } + }).component(); + + existingTableButton.onDidClick(() => { + if (this._tables) { + this._tables.enabled = existingTableButton.checked; + } + newTableName.enabled = !existingTableButton.checked; + this._existingTablesSelected = existingTableButton.checked || false; + }); + newTableButton.onDidClick(() => { + if (this._tables) { + this._tables.enabled = !newTableButton.checked; + } + newTableName.enabled = newTableButton.checked; + this._existingTablesSelected = existingTableButton.checked || false; + }); + newTableName.onTextChanged(async () => { + this._selectedTableName = newTableName.value || ''; + await this.onTableSelected(); + }); + this._tables.onValueChanged(async (value) => { // There's an issue with dropdown doesn't set the value in editable mode. this is the workaround if (this._tables && value) { - this._selectedTableName = this._settings.editable ? value : value.selected; + this._selectedTableName = value.selected; } await this.onTableSelected(); }); @@ -73,22 +116,32 @@ export class TableSelectionComponent extends ModelViewBase implements IDataCompo }], { info: databaseTitle }).withLayout({ padding: '0px' }).component(); - const tableForm = modelBuilder.formContainer().withFormItems([{ - title: tableTitle, - component: this._tables - }], { info: tableTitle }).withLayout({ - padding: '0px' - }).component(); + + const tableForm = modelBuilder.formContainer(); + if (this._settings.editable) { + tableForm.addFormItem({ + title: tableTitle, + component: group + }, { info: tableTitle }); + } else { + tableForm.addFormItem({ + title: tableTitle, + component: this._tables + }, { info: tableTitle }); + } + this._dbTableComponent = modelBuilder.flexContainer().withItems([ databaseForm, - tableForm + tableForm.withLayout({ + padding: '0px' + }).component() ], { flex: '0 0 auto', CSSStyles: { 'align-items': 'flex-start' } }).withLayout({ - flexFlow: 'row', + flexFlow: this._settings.editable ? 'column' : 'row', justifyContent: 'space-between', width: this.tableMaxLength }).component(); @@ -163,31 +216,33 @@ export class TableSelectionComponent extends ModelViewBase implements IDataCompo } private async onDatabaseSelected(): Promise { - this._tableNames = await this.listTableNames(this.databaseName || ''); - let tableNames = this._tableNames; + if (this._existingTablesSelected) { + this._tableNames = await this.listTableNames(this.databaseName || ''); + let tableNames = this._tableNames; - if (this._tableNames && !this._settings.preSelected && !this._tableNames.find(x => x.tableName === constants.selectTableTitle)) { - const firstRow: DatabaseTable = { tableName: constants.selectTableTitle, databaseName: '', schema: '' }; - tableNames = [firstRow].concat(this._tableNames); - } - - if (this._tables && tableNames && tableNames.length > 0) { - this._tables.values = tableNames.map(t => this.getTableFullName(t)); - if (this.importTable) { - const selectedTable = tableNames.find(t => t.tableName === this.importTable?.tableName && t.schema === this.importTable?.schema); - if (selectedTable) { - this._selectedTableName = this.getTableFullName(selectedTable); - this._tables.value = this.getTableFullName(selectedTable); - } else { - this._selectedTableName = this._settings.editable ? this.getTableFullName(this.importTable) : this.getTableFullName(tableNames[0]); - } - } else { - this._selectedTableName = this.getTableFullName(tableNames[0]); + if (this._tableNames && !this._settings.preSelected && !this._tableNames.find(x => x.tableName === constants.selectTableTitle)) { + const firstRow: DatabaseTable = { tableName: constants.selectTableTitle, databaseName: '', schema: '' }; + tableNames = [firstRow].concat(this._tableNames); + } + + if (this._tables && tableNames && tableNames.length > 0) { + this._tables.values = tableNames.map(t => this.getTableFullName(t)); + if (this.importTable) { + const selectedTable = tableNames.find(t => t.tableName === this.importTable?.tableName && t.schema === this.importTable?.schema); + if (selectedTable) { + this._selectedTableName = this.getTableFullName(selectedTable); + this._tables.value = this.getTableFullName(selectedTable); + } else { + this._selectedTableName = this._settings.editable ? this.getTableFullName(this.importTable) : this.getTableFullName(tableNames[0]); + } + } else { + this._selectedTableName = this.getTableFullName(tableNames[0]); + } + this._tables.value = this._selectedTableName; + } else if (this._tables) { + this._tables.values = []; + this._tables.value = ''; } - this._tables.value = this._selectedTableName; - } else if (this._tables) { - this._tables.values = []; - this._tables.value = ''; } await this.onTableSelected(); } diff --git a/extensions/machine-learning/src/views/widgets/dashboardWidget.ts b/extensions/machine-learning/src/views/widgets/dashboardWidget.ts index 702bab5b70..8455425d32 100644 --- a/extensions/machine-learning/src/views/widgets/dashboardWidget.ts +++ b/extensions/machine-learning/src/views/widgets/dashboardWidget.ts @@ -8,7 +8,6 @@ import * as vscode from 'vscode'; import { ApiWrapper } from '../../common/apiWrapper'; import * as path from 'path'; import * as constants from '../../common/constants'; -import * as utils from '../../common/utils'; import { PredictService } from '../../prediction/predictService'; interface IActionMetadata { @@ -182,12 +181,12 @@ export class DashboardWidget { }); const videosContainer = this.createVideoLinkContainers(view, [ { - iconPath: { light: 'images/video1.svg', dark: 'images/video1.svg' }, + iconPath: { light: 'images/aiMlSqlServer.svg', dark: 'images/aiMlSqlServer.svg' }, description: 'Artificial intelligence and machine learning with SQL Server 2019', link: 'https://www.youtube.com/watch?v=sE99cSoFOHs' }, { - iconPath: { light: 'images/video2.svg', dark: 'images/video2.svg' }, + iconPath: { light: 'images/sqlServerMl.svg', dark: 'images/sqlServerMl.svg' }, description: 'SQL Server Machine Learning Services', link: 'https://www.youtube.com/watch?v=R4GCBoxADyQ' } @@ -199,7 +198,7 @@ export class DashboardWidget { const moreVideosContainer = this.createVideoLinkContainers(view, [ { - iconPath: { light: 'images/video2.svg', dark: 'images/video2.svg' }, + iconPath: { light: 'images/notebooksIntro.svg', dark: 'images/notebooksIntro.svg' }, description: 'Introduction to Azure Data Studio Notebooks', link: 'https://www.youtube.com/watch?v=Nt4kIHQ0IOc' } @@ -316,7 +315,7 @@ export class DashboardWidget { 'background-position': 'top', 'width': `${maxWidth}px`, 'height': '110px', - 'background-size': `{maxWidth}px 120px` + 'background-size': `${maxWidth}px 120px` } }); videosContainer.addItem(descriptionComponent); @@ -349,15 +348,15 @@ export class DashboardWidget { link: constants.mlsDocLink }, { - title: constants.sqlMlsAzureDocTitle, - description: constants.sqlMlsAzureDocDesc, - link: constants.mlsAzureDocLink + title: constants.onnxOnEdgeOdbcDocTitle, + description: constants.onnxOnEdgeOdbcDocDesc, + link: constants.onnxOnEdgeDocs }]; const moreLink = { title: constants.mlsInstallOdbcDocTitle, description: constants.mlsInstallOdbcDocDesc, - link: utils.isWindows() ? constants.odbcDriverWindowsDocuments : constants.odbcDriverLinuxDocuments + link: constants.odbcDriverDocuments }; const styles = { 'padding': '10px' @@ -499,7 +498,7 @@ export class DashboardWidget { } private createTaskButton(view: azdata.ModelView, taskMetaData: IActionMetadata): azdata.Component { - const maxHeight = 106; + const maxHeight = 116; const maxWidth = 250; const mainContainer = view.modelBuilder.divContainer().withLayout({ width: maxWidth,