MLS - Changed the dashboard to match the design (#9905)

* Machine Learning Extension - Changed the dashboard to match the design
This commit is contained in:
Leila Lali
2020-04-13 14:06:29 -07:00
committed by GitHub
parent 8c491d36f4
commit 3f08d5d714
18 changed files with 625 additions and 83 deletions

View File

@@ -129,4 +129,8 @@ export class ApiWrapper {
public createButton(label: string, position?: azdata.window.DialogButtonPosition): azdata.window.Button {
return azdata.window.createButton(label, position);
}
public registerWidget(widgetId: string, handler: (view: azdata.ModelView) => void): void {
azdata.ui.registerModelViewProvider(widgetId, handler);
}
}

View File

@@ -16,7 +16,7 @@ export const rLPackagedFolderName = 'r_packages';
export const mlEnableMlsCommand = 'mls.command.enableMls';
export const mlDisableMlsCommand = 'mls.command.disableMls';
export const extensionOutputChannel = 'Machine Learning Services';
export const extensionOutputChannel = 'SQL Machine Learning';
export const notebookExtensionName = 'Microsoft.notebook';
export const azureSubscriptionsCommand = 'azure.accounts.getSubscriptions';
export const azureResourceGroupsCommand = 'azure.accounts.getResourceGroups';
@@ -26,11 +26,10 @@ export const azureResourceGroupsCommand = 'azure.accounts.getResourceGroups';
export const mlManageLanguagesCommand = 'mls.command.manageLanguages';
export const mlsPredictModelCommand = 'mls.command.predictModel';
export const mlManageModelsCommand = 'mls.command.manageModels';
export const mlRegisterModelCommand = 'mls.command.registerModel';
export const mlImportModelCommand = 'mls.command.importModel';
export const mlManagePackagesCommand = 'mls.command.managePackages';
export const mlOdbcDriverCommand = 'mls.command.odbcdriver';
export const mlsDocumentsCommand = 'mls.command.mlsdocs';
export const mlsDependenciesCommand = 'mls.command.dependencies';
export const notebookCommandNew = 'notebook.command.new';
// Configurations
//
@@ -148,8 +147,12 @@ export const currentModelsTitle = localize('models.currentModelsTitle', "Models"
export const azureRegisterModel = localize('models.azureRegisterModel', "Deploy");
export const predictModel = localize('models.predictModel', "Predict");
export const registerModelTitle = localize('models.RegisterWizard', "Deployed models");
export const deployModelTitle = localize('models.deployModelTitle', "Deploy models");
export const makePredictionTitle = localize('models.makePredictionTitle', "Make prediction");
export const importModelTitle = localize('models.importModelTitle', "Import models");
export const importModelDesc = localize('models.importModelDesc', "Build, import and expose a machine learning model");
export const makePredictionTitle = localize('models.makePredictionTitle', "Make predictions");
export const makePredictionDesc = localize('models.makePredictionDesc', "Generates a predicted value or scores using a managed model");
export const createNotebookTitle = localize('models.createNotebookTitle', "Create notebook");
export const createNotebookDesc = localize('models.createNotebookDesc', "Run experiments and create models");
export const modelRegisteredSuccessfully = localize('models.modelRegisteredSuccessfully', "Model registered successfully");
export const modelFailedToRegister = localize('models.modelFailedToRegistered', "Model failed to register");
export const localModelSource = localize('models.localModelSource', "File upload");
@@ -166,8 +169,15 @@ export function importModelFailedError(modelName: string | undefined, filePath:
export const loadModelParameterFailedError = localize('models.loadModelParameterFailedError', "Failed to load model parameters'");
export const unsupportedModelParameterType = localize('models.unsupportedModelParameterType', "unsupported");
export const dashboardTitle = localize('dashboardTitle', "SQL ML");
export const dashboardDesc = localize('dashboardDesc', "Machine learning for SQL databases");
export const dashboardLinksTitle = localize('dashboardLinksTitle', "Useful links");
export const dashboardVideoLinksTitle = localize('dashboardVideoLinksTitle', "Video tutorials");
export const learnMoreTitle = localize('learnMoreTitle', "Learn more");
export const mlsInstallMlsDocTitle = localize('mlsInstallMlsDocTitle', "Install SQL Server Machine Learning Services");
export const mlsInstallMlsDocDesc = localize('mlsInstallMlsDocDesc', "This document guides you in the installation of SQL Server Machine Learning Services. Python and R scripts can be executed in-database using Machine Learning Services.");
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.");
// Links
//

View File

@@ -22,6 +22,7 @@ import { DeployedModelService } from '../modelManagement/deployedModelService';
import { AzureModelRegistryService } from '../modelManagement/azureModelRegistryService';
import { ModelPythonClient } from '../modelManagement/modelPythonClient';
import { PredictService } from '../prediction/predictService';
import { DashboardWidget } from '../views/widgets/dashboardWidget';
/**
* The main controller class that initializes the extension
@@ -110,13 +111,16 @@ export default class MainController implements vscode.Disposable {
let modelManagementController = new ModelManagementController(this._apiWrapper, this._rootPath,
azureModelsService, registeredModelService, predictService);
let dashboardWidget = new DashboardWidget(this._apiWrapper, this._rootPath);
dashboardWidget.register();
this._apiWrapper.registerCommand(constants.mlManageLanguagesCommand, (async () => {
await languageController.manageLanguages();
}));
this._apiWrapper.registerCommand(constants.mlManageModelsCommand, (async () => {
await modelManagementController.manageRegisteredModels();
}));
this._apiWrapper.registerCommand(constants.mlRegisterModelCommand, (async () => {
this._apiWrapper.registerCommand(constants.mlImportModelCommand, (async () => {
await modelManagementController.registerModel();
}));
this._apiWrapper.registerCommand(constants.mlsPredictModelCommand, (async () => {
@@ -134,18 +138,12 @@ export default class MainController implements vscode.Disposable {
this._apiWrapper.registerTaskHandler(constants.mlManageModelsCommand, async () => {
await modelManagementController.manageRegisteredModels();
});
this._apiWrapper.registerTaskHandler(constants.mlRegisterModelCommand, async () => {
this._apiWrapper.registerTaskHandler(constants.mlImportModelCommand, async () => {
await modelManagementController.registerModel();
});
this._apiWrapper.registerTaskHandler(constants.mlsPredictModelCommand, async () => {
await modelManagementController.predictModel();
});
this._apiWrapper.registerTaskHandler(constants.mlOdbcDriverCommand, async () => {
await this.packageManagementService.openOdbcDriverDocuments();
});
this._apiWrapper.registerTaskHandler(constants.mlsDocumentsCommand, async () => {
await this.packageManagementService.openDocuments();
});
}
/**

View File

@@ -29,28 +29,6 @@ export class PackageManagementService {
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
*/

View File

@@ -31,20 +31,6 @@ describe('Package Management Service', () => {
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 PackageManagementService(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 PackageManagementService(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));

View File

@@ -0,0 +1,39 @@
/*---------------------------------------------------------------------------------------------
* 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 { createViewContext } from './utils';
import { DashboardWidget } from '../../views/widgets/dashboardWidget';
interface TestContext {
apiWrapper: TypeMoq.IMock<ApiWrapper>;
view: azdata.ModelView;
onClick: vscode.EventEmitter<any>;
}
function createContext(): TestContext {
let viewTestContext = createViewContext();
return {
apiWrapper: viewTestContext.apiWrapper,
view: viewTestContext.view,
onClick: viewTestContext.onClick
};
}
describe('Dashboard widget', () => {
it('Should create view components successfully ', async function (): Promise<void> {
let testContext = createContext();
const dashboard = new DashboardWidget(testContext.apiWrapper.object, '');
dashboard.register();
testContext.onClick.fire();
testContext.apiWrapper.verify(x => x.executeCommand(TypeMoq.It.isAny()), TypeMoq.Times.atMostOnce());
});
});

View File

@@ -52,6 +52,9 @@ export function createViewContext(): ViewTestContext {
});
let flex: azdata.FlexContainer = Object.assign({}, componentBase, container, {
});
let div: azdata.DivContainer = Object.assign({}, componentBase, container, {
onDidClick: onClick.event
});
let buttonBuilder: azdata.ComponentBuilder<azdata.ButtonComponent> = {
component: () => button,
@@ -134,6 +137,13 @@ export function createViewContext(): ViewTestContext {
withItems: () => flexBuilder,
withLayout: () => flexBuilder
});
let divBuilder: azdata.DivBuilder = Object.assign({}, {
component: () => div,
withProperties: () => divBuilder,
withValidation: () => divBuilder,
withItems: () => divBuilder,
withLayout: () => divBuilder
});
let inputBoxBuilder: azdata.ComponentBuilder<azdata.InputBoxComponent> = {
component: () => {
@@ -180,7 +190,7 @@ export function createViewContext(): ViewTestContext {
modelBuilder: {
radioCardGroup: undefined!,
navContainer: undefined!,
divContainer: undefined!,
divContainer: () => divBuilder,
flexContainer: () => flexBuilder,
splitViewContainer: undefined!,
dom: undefined!,
@@ -295,6 +305,13 @@ export function createViewContext(): ViewTestContext {
apiWrapper.setup(x => x.createWizardPage(TypeMoq.It.isAny())).returns(() => wizardPage);
apiWrapper.setup(x => x.createModelViewDialog(TypeMoq.It.isAny())).returns(() => dialog);
apiWrapper.setup(x => x.openDialog(TypeMoq.It.isAny())).returns(() => { });
apiWrapper.setup(x => x.registerWidget(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(async (id, handler) => {
if (id) {
return await handler(view);
} else {
Promise.reject();
}
});
return {
apiWrapper: apiWrapper,

View File

@@ -31,7 +31,7 @@ export class RegisteredModelsDialog extends ModelViewBase {
this.currentLanguagesTab = new CurrentModelsPage(this._apiWrapper, this);
let registerModelButton = this._apiWrapper.createButton(constants.deployModelTitle);
let registerModelButton = this._apiWrapper.createButton(constants.importModelTitle);
registerModelButton.onClick(async () => {
await this.sendDataRequest(RegisterModelEventName);
});

View File

@@ -0,0 +1,486 @@
/*---------------------------------------------------------------------------------------------
* 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 * as path from 'path';
import * as constants from '../../common/constants';
import * as utils from '../../common/utils';
interface IActionMetadata {
title?: string,
description?: string,
link?: string,
iconPath?: { light: string | vscode.Uri; dark: string | vscode.Uri },
command?: string;
}
const maxWidth = 800;
const headerMaxHeight = 200;
export class DashboardWidget {
/**
* Creates new instance of dashboard
*/
constructor(private _apiWrapper: ApiWrapper, private _root: string) {
}
public register(): void {
this._apiWrapper.registerWidget('mls.dashboard', async (view) => {
const container = view.modelBuilder.flexContainer().withLayout({
flexFlow: 'column',
width: '100%',
height: '100%'
}).component();
const header = this.createHeader(view);
const tasksContainer = this.createTasks(view);
const footerContainer = this.createFooter(view);
container.addItem(header, {
CSSStyles: {
'background-image': `url(${vscode.Uri.file(this.asAbsolutePath('images/background.svg'))})`,
'background-repeat': 'no-repeat',
'background-position': 'top',
'width': `${maxWidth}px`,
'height': '130px',
'background-size': `${maxWidth}px ${headerMaxHeight}px`
}
});
container.addItem(tasksContainer, {
CSSStyles: {
'width': `${maxWidth}px`,
'height': '150px',
}
});
container.addItem(footerContainer, {
CSSStyles: {
'width': `${maxWidth}px`,
'height': '500px',
}
});
const mainContainer = view.modelBuilder.flexContainer()
.withLayout({
flexFlow: 'column',
width: '100%',
height: '100%',
position: 'absolute'
}).component();
mainContainer.addItem(container, {
CSSStyles: { 'padding-top': '25px', 'padding-left': '5px' }
});
await view.initializeModel(mainContainer);
});
}
private createHeader(view: azdata.ModelView): azdata.Component {
const header = view.modelBuilder.flexContainer().withLayout({
flexFlow: 'column',
width: maxWidth,
height: headerMaxHeight,
}).component();
const titleComponent = view.modelBuilder.text().withProperties({
value: constants.dashboardTitle,
CSSStyles: {
'font-size': '36px',
//'color': '#333333',
'font-weight': 'bold',
'margin': '0px'
}
}).component();
const descComponent = view.modelBuilder.text().withProperties({
value: constants.dashboardDesc,
CSSStyles: {
'font-size': '14px',
//'color': '#888888',
'font-weight': 'bold',
'margin': '0px'
}
}).component();
header.addItems([titleComponent, descComponent], {
CSSStyles: {
'width': `${maxWidth}px`,
'padding': '10px'
}
});
return header;
}
private createFooter(view: azdata.ModelView): azdata.Component {
const footerContainer = view.modelBuilder.flexContainer().withLayout({
flexFlow: 'row',
width: maxWidth,
height: '500px',
justifyContent: 'flex-start'
}).component();
const linksContainer = this.createLinks(view);
const videoLinksContainer = this.createVideoLinks(view);
footerContainer.addItem(linksContainer);
footerContainer.addItem(videoLinksContainer, {
CSSStyles: {
'padding-left': '50px',
}
});
return footerContainer;
}
private createVideoLinks(view: azdata.ModelView): azdata.Component {
const maxWidth = 400;
const linksContainer = view.modelBuilder.flexContainer().withLayout({
flexFlow: 'column',
width: maxWidth,
height: '500px',
justifyContent: 'flex-start'
}).component();
const titleComponent = view.modelBuilder.text().withProperties({
value: constants.dashboardVideoLinksTitle,
CSSStyles: {
'font-size': '18px',
'font-weight': 'bold',
'margin': '0px'
}
}).component();
const videosContainer = view.modelBuilder.flexContainer().withLayout({
flexFlow: 'row',
width: maxWidth,
height: '500px',
}).component();
const video1Container = this.createVideoLink(view, {
iconPath: { light: 'images/video1.svg', dark: 'images/video1.svg' },
description: 'Visualize data using SandDance',
link: 'https://www.youtube.com/watch?v=e305wTAoLZs'
});
videosContainer.addItem(video1Container);
const video2Container = this.createVideoLink(view, {
iconPath: { light: 'images/video2.svg', dark: 'images/video2.svg' },
description: 'How to make the best out of Microsoft Azure'
});
videosContainer.addItem(video2Container);
linksContainer.addItems([titleComponent], {
CSSStyles: {
'padding': '0px',
'padding-right': '5px',
'padding-top': '10px',
'height': '10px',
'margin': '0px'
}
});
linksContainer.addItems([videosContainer], {
CSSStyles: {
'padding': '0px',
'padding-right': '5px',
'padding-top': '10px',
'height': '10px',
'margin': '0px'
}
});
return linksContainer;
}
private createVideoLink(view: azdata.ModelView, linkMetaData: IActionMetadata): azdata.Component {
const maxWidth = 200;
const videosContainer = view.modelBuilder.flexContainer().withLayout({
flexFlow: 'column',
width: maxWidth,
height: '200px',
justifyContent: 'flex-start'
}).component();
const video1Container = view.modelBuilder.divContainer().withProperties({
clickable: true,
width: maxWidth,
height: '100px'
}).component();
const descriptionComponent = view.modelBuilder.text().withProperties({
value: linkMetaData.description,
width: '200px',
height: '50px',
CSSStyles: {
//'color': '#605E5C',
'font-size': '12px',
'margin': '0px'
}
}).component();
video1Container.onDidClick(async () => {
if (linkMetaData.link) {
await this._apiWrapper.openExternal(vscode.Uri.parse(linkMetaData.link));
}
});
videosContainer.addItem(video1Container, {
CSSStyles: {
'background-image': `url(${vscode.Uri.file(this.asAbsolutePath(<string>linkMetaData.iconPath?.light || ''))})`,
'background-repeat': 'no-repeat',
'background-position': 'top',
'width': `150px`,
'height': '110px',
'background-size': `150px 120px`
}
});
videosContainer.addItem(descriptionComponent);
return videosContainer;
}
private createLinks(view: azdata.ModelView): azdata.Component {
const maxWidth = 400;
const linksContainer = view.modelBuilder.flexContainer().withLayout({
flexFlow: 'column',
width: maxWidth,
height: '500px',
}).component();
const titleComponent = view.modelBuilder.text().withProperties({
value: constants.dashboardLinksTitle,
CSSStyles: {
'font-size': '18px',
//'color': '#323130',
'font-weight': 'bold',
'margin': '0px'
}
}).component();
let mlsLink: string;
if (utils.isWindows()) {
mlsLink = constants.installMlsWindowsDocs;
} else {
mlsLink = constants.installMlsLinuxDocs;
}
const mlsDocs = this.createLink(view, {
title: constants.mlsInstallMlsDocTitle,
description: constants.mlsInstallMlsDocDesc,
link: mlsLink
});
let odbcLink: string;
if (utils.isWindows()) {
odbcLink = constants.odbcDriverWindowsDocuments;
} else {
odbcLink = constants.odbcDriverLinuxDocuments;
}
const rdbcDocs = this.createLink(view, {
title: constants.mlsInstallOdbcDocTitle,
description: constants.mlsInstallOdbcDocDesc,
link: odbcLink
});
linksContainer.addItems([titleComponent, mlsDocs, rdbcDocs], {
CSSStyles: {
'padding': '10px'
}
});
return linksContainer;
}
private createLink(view: azdata.ModelView, linkMetaData: IActionMetadata): azdata.Component {
const maxHeight = 80;
const maxWidth = 400;
const labelsContainer = view.modelBuilder.flexContainer().withLayout({
flexFlow: 'column',
width: maxWidth,
height: maxHeight,
justifyContent: 'flex-start'
}).component();
const descriptionComponent = view.modelBuilder.text().withProperties({
value: linkMetaData.description,
width: maxWidth,
CSSStyles: {
//'color': '#605E5C',
'font-size': '12px',
'margin': '0px'
}
}).component();
const linkContainer = view.modelBuilder.flexContainer().withLayout({
flexFlow: 'row',
width: maxWidth,
justifyContent: 'flex-start'
}).component();
const linkComponent = view.modelBuilder.hyperlink().withProperties({
label: linkMetaData.title,
url: linkMetaData.link,
CSSStyles: {
'color': '#3794ff',
'font-size': '14px',
'margin': '0px'
}
}).component();
const image = view.modelBuilder.image().withProperties({
width: '10px',
height: '10px',
iconPath: {
dark: this.asAbsolutePath('images/linkIcon.svg'),
light: this.asAbsolutePath('images/linkIcon.svg'),
},
iconHeight: '10px',
iconWidth: '10px'
}).component();
linkContainer.addItem(linkComponent, {
CSSStyles: {
'padding': '0px',
'padding-right': '5px',
'height': '10px',
'margin': '0px'
}
});
linkContainer.addItem(image, {
CSSStyles: {
'padding': '0px',
'padding-right': '5px',
'padding-top': '5px',
'height': '10px',
'margin': '0px'
}
});
labelsContainer.addItems([linkContainer, descriptionComponent], {
CSSStyles: {
'padding': '0px',
'padding-top': '5px',
'margin': '0px'
}
});
return labelsContainer;
}
private asAbsolutePath(filePath: string): string {
return path.join(this._root || '', filePath);
}
private createTasks(view: azdata.ModelView): azdata.Component {
const tasksContainer = view.modelBuilder.flexContainer().withLayout({
flexFlow: 'row',
width: '100%',
height: '50px',
}).component();
const predictionMetadata: IActionMetadata = {
title: constants.makePredictionTitle,
description: constants.makePredictionDesc,
iconPath: {
dark: this.asAbsolutePath('images/makePredictions.svg'),
light: this.asAbsolutePath('images/makePredictions.svg'),
},
link: '',
command: constants.mlsPredictModelCommand
};
const predictionButton = this.createTaskButton(view, predictionMetadata);
const importMetadata: IActionMetadata = {
title: constants.importModelTitle,
description: constants.importModelDesc,
iconPath: {
dark: this.asAbsolutePath('images/makePredictions.svg'),
light: this.asAbsolutePath('images/makePredictions.svg'),
},
link: '',
command: constants.mlImportModelCommand
};
const importModelsButton = this.createTaskButton(view, importMetadata);
const notebookMetadata: IActionMetadata = {
title: constants.createNotebookTitle,
description: constants.createNotebookDesc,
iconPath: {
dark: this.asAbsolutePath('images/createNotebook.svg'),
light: this.asAbsolutePath('images/createNotebook.svg'),
},
link: '',
command: constants.notebookCommandNew
};
const notebookModelsButton = this.createTaskButton(view, notebookMetadata);
tasksContainer.addItems([predictionButton, importModelsButton, notebookModelsButton], {
CSSStyles: {
'padding': '10px'
}
});
return tasksContainer;
}
private createTaskButton(view: azdata.ModelView, taskMetaData: IActionMetadata): azdata.Component {
const maxHeight = 106;
const maxWidth = 250;
const mainContainer = view.modelBuilder.divContainer().withLayout({
width: maxWidth,
height: maxHeight
}).withProperties({
clickable: true,
ariaRole: taskMetaData.title
}).component();
const iconContainer = view.modelBuilder.flexContainer().withLayout({
flexFlow: 'row',
width: maxWidth,
height: maxHeight - 20,
alignItems: 'flex-start'
}).component();
const labelsContainer = view.modelBuilder.flexContainer().withLayout({
flexFlow: 'column',
width: maxWidth - 50,
height: maxHeight - 20,
justifyContent: 'space-between'
}).component();
const titleComponent = view.modelBuilder.text().withProperties({
value: taskMetaData.title,
CSSStyles: {
'font-size': '14px',
//'color': '#323130',
'font-weight': 'bold',
'margin': '0px'
}
}).component();
const descriptionComponent = view.modelBuilder.text().withProperties({
value: taskMetaData.description,
CSSStyles: {
//'color': '#605E5C',
'font-size': '13px',
'margin': '0px'
}
}).component();
const linkComponent = view.modelBuilder.hyperlink().withProperties({
label: constants.learnMoreTitle,
url: taskMetaData.link,
CSSStyles: {
//'background-color': '#F2F2F2',
'color': '#3794ff',
'margin': '0px'
}
}).component();
const image = view.modelBuilder.image().withProperties({
width: '20px',
height: '20px',
iconPath: taskMetaData.iconPath,
iconHeight: '20px',
iconWidth: '20px'
}).component();
labelsContainer.addItems([titleComponent, descriptionComponent, linkComponent], {
CSSStyles: {
'padding': '0px',
'padding-bottom': '5px',
'width': '180px',
'margin': '0px'
}
});
iconContainer.addItem(image, {
CSSStyles: {
'padding-top': '10px',
'padding-right': '10px'
}
});
iconContainer.addItem(labelsContainer, {
CSSStyles: {
'padding-top': '5px',
'padding-right': '10px'
}
});
mainContainer.addItems([iconContainer], {
CSSStyles: {
//'background-color': '#f4f4f4',
'padding': '10px',
'border-radius': '5px',
'border-color': '#f2f2f2',
'border': '1px solid'
}
});
mainContainer.onDidClick(async () => {
if (taskMetaData.command) {
await this._apiWrapper.executeCommand(taskMetaData.command);
}
});
return mainContainer;
}
}