From 429d8fe5847c0ab5520677c76702f81433ae8d4c Mon Sep 17 00:00:00 2001 From: Leila Lali Date: Wed, 28 Oct 2020 11:57:07 -0700 Subject: [PATCH] ML - New UI component for icon, title and description of an item (#13109) * initial checkin * addressed PR comment --- .../machine-learning/images/emptyTable.svg | 30 ++++ .../machine-learning/images/invalidItem.svg | 3 + .../machine-learning/images/validItem.svg | 4 + .../machine-learning/src/common/constants.ts | 4 +- .../src/views/dataInfoComponent.ts | 163 ++++++++++++++++++ .../src/views/models/azureModelsComponent.ts | 40 ++++- .../src/views/models/azureModelsTable.ts | 52 +++--- .../models/azureResourceFilterComponent.ts | 37 +--- .../manageModels/modelImportLocationPage.ts | 78 ++++----- 9 files changed, 313 insertions(+), 98 deletions(-) create mode 100644 extensions/machine-learning/images/emptyTable.svg create mode 100644 extensions/machine-learning/images/invalidItem.svg create mode 100644 extensions/machine-learning/images/validItem.svg create mode 100644 extensions/machine-learning/src/views/dataInfoComponent.ts diff --git a/extensions/machine-learning/images/emptyTable.svg b/extensions/machine-learning/images/emptyTable.svg new file mode 100644 index 0000000000..15ce8973bc --- /dev/null +++ b/extensions/machine-learning/images/emptyTable.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/extensions/machine-learning/images/invalidItem.svg b/extensions/machine-learning/images/invalidItem.svg new file mode 100644 index 0000000000..9f6ebdda6d --- /dev/null +++ b/extensions/machine-learning/images/invalidItem.svg @@ -0,0 +1,3 @@ + + + diff --git a/extensions/machine-learning/images/validItem.svg b/extensions/machine-learning/images/validItem.svg new file mode 100644 index 0000000000..6187cb83d1 --- /dev/null +++ b/extensions/machine-learning/images/validItem.svg @@ -0,0 +1,4 @@ + + + + diff --git a/extensions/machine-learning/src/common/constants.ts b/extensions/machine-learning/src/common/constants.ts index 08b0d84194..9df654e946 100644 --- a/extensions/machine-learning/src/common/constants.ts +++ b/extensions/machine-learning/src/common/constants.ts @@ -150,6 +150,8 @@ export const extLangUpdateFailedError = localize('extLang.updateFailedError', "F export const modelUpdateFailedError = localize('models.modelUpdateFailedError', "Failed to update the model"); export const modelsListEmptyMessage = localize('models.modelsListEmptyMessage', "No models yet"); +export const azureModelsListEmptyTitle = localize('models.azureModelsListEmptyTitle', "No models found"); +export const azureModelsListEmptyDescription = localize('models.azureModelsListEmptyDescription', "Select another Azure ML workspace"); export const modelsListEmptyDescription = localize('models.modelsListEmptyDescription', "Use import wizard to add models to this table"); export const databaseName = localize('databaseName', "Models database"); export const databaseToStoreInfo = localize('databaseToStoreInfo', "Select a database to store the new model."); @@ -234,7 +236,7 @@ export const modelsRequiredError = localize('models.modelsRequiredError', "Pleas export const updateModelFailedError = localize('models.updateModelFailedError', "Failed to update the model"); export const modelSchemaIsAcceptedMessage = localize('models.modelSchemaIsAcceptedMessage', "Table meets requirements!"); export const selectModelsTableMessage = localize('models.selectModelsTableMessage', "Select models table"); -export const modelSchemaIsNotAcceptedMessage = localize('models.modelSchemaIsNotAcceptedMessage', "Invalid table structure"); +export const modelSchemaIsNotAcceptedMessage = localize('models.modelSchemaIsNotAcceptedMessage', "Invalid table structure!"); export function importModelFailedError(modelName: string | undefined, filePath: string | undefined): string { return localize('models.importModelFailedError', "Failed to register the model: {0} ,file: {1}", modelName || '', filePath || ''); } export function invalidImportTableError(databaseName: string | undefined, tableName: string | undefined): string { return localize('models.invalidImportTableError', "Invalid table for importing models. database name: {0} ,table name: {1}", databaseName || '', tableName || ''); } export function invalidImportTableSchemaError(databaseName: string | undefined, tableName: string | undefined): string { return localize('models.invalidImportTableSchemaError', "Table schema is not supported for model import. Database name: {0}, table name: {1}.", databaseName || '', tableName || ''); } diff --git a/extensions/machine-learning/src/views/dataInfoComponent.ts b/extensions/machine-learning/src/views/dataInfoComponent.ts new file mode 100644 index 0000000000..6cf26047c1 --- /dev/null +++ b/extensions/machine-learning/src/views/dataInfoComponent.ts @@ -0,0 +1,163 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { ApiWrapper } from '../common/apiWrapper'; +import { ModelViewBase } from './models/modelViewBase'; +import { ViewBase } from './viewBase'; + + +export interface iconSettings { + width?: number, + height?: number, + css?: { [key: string]: string }, + path?: string | vscode.Uri | { light: string | vscode.Uri; dark: string | vscode.Uri } +} +/** + * View to pick model source + */ +export class DataInfoComponent extends ViewBase { + private _labelContainer: azdata.FlexContainer | undefined; + private _labelComponent: azdata.TextComponent | undefined; + private _descriptionComponent: azdata.TextComponent | undefined; + private _loadingComponent: azdata.LoadingComponent | undefined; + private _width: number = 100; + private _height: number = 100; + private _title: string = ''; + private _description: string = ''; + private _iconComponent: azdata.ImageComponent | undefined; + private _iconSettings: iconSettings | undefined; + + + constructor(apiWrapper: ApiWrapper, parent: ModelViewBase) { + super(apiWrapper, parent.root, parent); + } + + public registerComponent(modelBuilder: azdata.ModelBuilder): azdata.Component { + this._descriptionComponent = modelBuilder.text().withProperties({ + width: 200 + }).component(); + this._labelComponent = modelBuilder.text().withProperties({ + width: 200 + }).component(); + this._labelContainer = modelBuilder.flexContainer().withLayout({ + flexFlow: 'column', + width: this._width, + height: this._height, + justifyContent: 'center', + alignItems: 'center', + textAlign: 'center' + }).component(); + + if (!this._iconSettings) { + this._iconSettings = { + css: {}, + height: 50, + width: 50, + path: '' + , + }; + } + + this._iconComponent = modelBuilder.image().withProperties({ + width: 100, + height: 100, + iconWidth: this._iconSettings.width, + iconHeight: this._iconSettings.height, + title: this._title + }).component(); + let iconContainer = modelBuilder.flexContainer().withLayout({ + width: 100, + }).component(); + + iconContainer.addItem(this._iconComponent, { + CSSStyles: this._iconSettings.css + }); + + this._labelContainer.addItem(iconContainer); + this._labelContainer.addItem( + this._labelComponent + , { + CSSStyles: { + 'font-size': '16px' + } + }); + this._labelContainer.addItem( + this._descriptionComponent + , { + CSSStyles: { + 'font-size': '13px' + } + }); + + this._loadingComponent = modelBuilder.loadingComponent().withItem( + this._labelContainer + ).withProperties({ + loading: false + }).component(); + + return this._loadingComponent; + } + + public set width(value: number) { + this._width = value; + } + + public set height(value: number) { + this._height = value; + } + + public set title(value: string) { + this._title = value; + } + + public set description(value: string) { + this._description = value; + } + + public set iconSettings(value: iconSettings) { + this._iconSettings = value; + } + + public get iconSettings(): iconSettings { + return this._iconSettings || {}; + } + + public get component(): azdata.Component | undefined { + return this._loadingComponent; + } + + public loading(): void { + if (this._loadingComponent) { + this._loadingComponent.loading = true; + } + } + + public loaded(): void { + if (this._loadingComponent) { + this._loadingComponent.loading = false; + } + } + + public async refresh(): Promise { + this.loaded(); + if (this._labelComponent) { + this._labelComponent.value = this._title; + } + if (this._descriptionComponent) { + this._descriptionComponent.value = this._description; + } + + if (this._iconComponent) { + this._iconComponent.iconPath = this._iconSettings?.path; + } + if (this._labelContainer) { + this._labelContainer.height = this._height; + this._labelContainer.width = this._width; + } + return Promise.resolve(); + } +} diff --git a/extensions/machine-learning/src/views/models/azureModelsComponent.ts b/extensions/machine-learning/src/views/models/azureModelsComponent.ts index 557981c738..8ac45799a2 100644 --- a/extensions/machine-learning/src/views/models/azureModelsComponent.ts +++ b/extensions/machine-learning/src/views/models/azureModelsComponent.ts @@ -11,6 +11,8 @@ import { AzureModelsTable } from './azureModelsTable'; import { IDataComponent, AzureModelResource } from '../interfaces'; import { ModelArtifact } from './prediction/modelArtifact'; import { AzureSignInComponent } from './azureSignInComponent'; +import { DataInfoComponent } from '../dataInfoComponent'; +import * as constants from '../../common/constants'; export class AzureModelsComponent extends ModelViewBase implements IDataComponent { @@ -21,6 +23,7 @@ export class AzureModelsComponent extends ModelViewBase implements IDataComponen private _loader: azdata.LoadingComponent | undefined; private _form: azdata.FormContainer | undefined; private _downloadedFile: ModelArtifact | undefined; + private _emptyModelsComponent: DataInfoComponent | undefined; /** * Component to render a view to pick an azure model @@ -49,9 +52,32 @@ export class AzureModelsComponent extends ModelViewBase implements IDataComponen this._downloadedFile = undefined; }); + this._emptyModelsComponent = new DataInfoComponent(this._apiWrapper, this); + this._emptyModelsComponent.width = 300; + this._emptyModelsComponent.height = 250; + this._emptyModelsComponent.iconSettings = { + css: { 'padding-top': '20px' }, + width: 100, + height: 100 + }; + + this._emptyModelsComponent.registerComponent(modelBuilder); + this.azureFilterComponent.onWorkspacesSelectedChanged(async () => { await this.onLoading(); await this.azureModelsTable?.loadData(this.azureFilterComponent?.data); + if (this._emptyModelsComponent) { + if (this.azureModelsTable?.isTableEmpty) { + this._emptyModelsComponent.title = constants.azureModelsListEmptyTitle; + this._emptyModelsComponent.description = constants.azureModelsListEmptyDescription; + this._emptyModelsComponent.iconSettings.path = this.asAbsolutePath('images/emptyTable.svg'); + } else { + this._emptyModelsComponent.title = ''; + this._emptyModelsComponent.description = ''; + this._emptyModelsComponent.iconSettings.path = 'noicon'; + } + await this._emptyModelsComponent.refresh(); + } await this.onLoaded(); }); @@ -80,23 +106,31 @@ export class AzureModelsComponent extends ModelViewBase implements IDataComponen } private addAzureComponents(formBuilder: azdata.FormBuilder) { - if (this.azureFilterComponent && this._loader) { + if (this.azureFilterComponent && this._loader && this._emptyModelsComponent?.component) { this.azureFilterComponent.addComponents(formBuilder); formBuilder.addFormItems([{ title: '', component: this._loader - }]); + }], { horizontal: true }); + formBuilder.addFormItems([{ + title: '', + component: this._emptyModelsComponent.component + }], { horizontal: true }); } } private removeAzureComponents(formBuilder: azdata.FormBuilder) { - if (this.azureFilterComponent && this._loader) { + if (this.azureFilterComponent && this._loader && this._emptyModelsComponent?.component) { this.azureFilterComponent.removeComponents(formBuilder); formBuilder.removeFormItem({ title: '', component: this._loader }); + formBuilder.removeFormItem({ + title: '', + component: this._emptyModelsComponent.component + }); } } diff --git a/extensions/machine-learning/src/views/models/azureModelsTable.ts b/extensions/machine-learning/src/views/models/azureModelsTable.ts index eb2e8f7ef7..1dd41a47aa 100644 --- a/extensions/machine-learning/src/views/models/azureModelsTable.ts +++ b/extensions/machine-learning/src/views/models/azureModelsTable.ts @@ -39,6 +39,18 @@ export class AzureModelsTable extends ModelViewBase implements IDataComponent( { columns: [ + { // Action + displayName: '', + valueType: azdata.DeclarativeDataType.component, + isReadOnly: true, + width: 50, + headerCssStyles: { + ...constants.cssStyles.tableHeader + }, + rowCssStyles: { + ...constants.cssStyles.tableRow + }, + }, { // Name displayName: constants.modelName, ariaLabel: constants.modelName, @@ -90,18 +102,6 @@ export class AzureModelsTable extends ModelViewBase implements IDataComponent { - if (this._table && workspaceResource) { - this._models = await this.listAzureModels(workspaceResource); - let tableData: any[][] = []; + if (this._table) { + if (workspaceResource) { + this._models = await this.listAzureModels(workspaceResource); + let tableData: any[][] = []; - if (this._models) { - tableData = tableData.concat(this._models.map(model => this.createTableRow(model))); + if (this._models) { + tableData = tableData.concat(this._models.map(model => this.createTableRow(model))); + } + + if (this.isTableEmpty) { + this._table.dataValues = []; + } else { + this._table.data = tableData; + } + } else { + this._table.dataValues = []; } - - this._table.data = tableData; } this._onModelSelectionChanged.fire(); } + public get isTableEmpty(): boolean { + return !this._models || this._models.length === 0; + } + private createTableRow(model: WorkspaceModel): any[] { if (this._modelBuilder) { let selectModelButton: azdata.Component; @@ -172,7 +184,7 @@ export class AzureModelsTable extends ModelViewBase implements IDataComponent { await this.onTableSelected(); @@ -85,10 +59,12 @@ export class ModelImportLocationPage extends ModelViewBase implements IPageView, this.tableSelectionComponent.registerComponent(modelBuilder); this.tableSelectionComponent.addComponents(this._formBuilder); - this._formBuilder.addFormItem({ - title: '', - component: this._labelContainer - }); + if (this._dataInfoComponent.component) { + this._formBuilder.addFormItem({ + title: '', + component: this._dataInfoComponent.component + }); + } this._form = this._formBuilder.component(); return this._form; } @@ -98,22 +74,28 @@ export class ModelImportLocationPage extends ModelViewBase implements IPageView, this.importTable = this.tableSelectionComponent?.data; } - if (this.importTable && this._labelComponent) { + if (this.importTable && this._dataInfoComponent) { + this._dataInfoComponent.loading(); // Add table name to the models imported. // Since Table name is picked last as per new flow this hasn't been set yet. this.modelsViewData?.forEach(x => x.targetImportTable = this.importTable); if (!this.validateImportTableName()) { - this._labelComponent.value = constants.selectModelsTableMessage; + this._dataInfoComponent.title = constants.selectModelsTableMessage; + this._dataInfoComponent.iconSettings.path = 'noicon'; } else { const validated = await this.verifyImportConfigTable(this.importTable); if (validated) { - this._labelComponent.value = constants.modelSchemaIsAcceptedMessage; + this._dataInfoComponent.title = constants.modelSchemaIsAcceptedMessage; + this._dataInfoComponent.iconSettings.path = this.asAbsolutePath('images/validItem.svg'); } else { - this._labelComponent.value = constants.modelSchemaIsNotAcceptedMessage; + this._dataInfoComponent.title = constants.modelSchemaIsNotAcceptedMessage; + this._dataInfoComponent.iconSettings.path = this.asAbsolutePath('images/invalidItem.svg'); } } + + await this._dataInfoComponent.refresh(); } } @@ -143,6 +125,10 @@ export class ModelImportLocationPage extends ModelViewBase implements IPageView, if (this.tableSelectionComponent) { await this.tableSelectionComponent.refresh(); } + + if (this._dataInfoComponent) { + await this._dataInfoComponent.refresh(); + } } /**