resource deployment ext implementation -wip (#5508)

* resource types

* implement the dialog

* remove unused method

* fix issues

* formatting

* 5-17

* address comments and more tests
This commit is contained in:
Alan Ren
2019-05-17 20:24:02 -07:00
committed by GitHub
parent a59d1d3c05
commit 586fe10525
36 changed files with 2208 additions and 21 deletions

View File

@@ -3,3 +3,77 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
export interface ResourceType {
name: string;
displayName: string;
description: string;
platforms: string[];
icon: { light: string; dark: string };
options: ResourceTypeOption[];
providers: DeploymentProvider[];
getProvider(selectedOptions: { option: string, value: string }[]): DeploymentProvider | undefined;
}
export interface ResourceTypeOption {
name: string;
displayName: string;
values: ResourceTypeOptionValue[];
}
export interface ResourceTypeOptionValue {
name: string;
displayName: string;
}
export interface DeploymentProvider {
notebook: string | NotebookInfo;
requiredTools: ToolRequirementInfo[];
when: string;
}
export interface NotebookInfo {
win32: string;
darwin: string;
linux: string;
}
export interface ToolRequirementInfo {
name: string;
version: string;
}
export enum ToolType {
Unknown,
AzCli,
KubeCtl,
Docker,
Python,
MSSQLCtl
}
export interface ToolStatusInfo {
type: ToolType;
name: string;
description: string;
version: string;
status: ToolInstallationStatus;
}
export interface ITool {
readonly name: string;
readonly displayName: string;
readonly description: string;
readonly type: ToolType;
readonly supportAutoInstall: boolean;
getInstallationStatus(versionExpression: string): Thenable<ToolInstallationStatus>;
install(version: string): Thenable<void>;
}
export enum ToolInstallationStatus {
NotInstalled,
Installed,
Installing,
FailedToInstall
}

View File

@@ -6,15 +6,45 @@
import vscode = require('vscode');
import { ResourceDeploymentDialog } from './ui/resourceDeploymentDialog';
import { ToolsService } from './services/toolsService';
import { NotebookService } from './services/notebookService';
import { ResourceTypeService } from './services/resourceTypeService';
import { PlatformService } from './services/platformService';
import * as nls from 'vscode-nls';
const localize = nls.loadMessageBundle();
export function activate(context: vscode.ExtensionContext) {
const platformService = new PlatformService();
const toolsService = new ToolsService();
const notebookService = new NotebookService(platformService);
const resourceTypeService = new ResourceTypeService(platformService, toolsService);
const resourceTypes = resourceTypeService.getResourceTypes();
const validationFailures = resourceTypeService.validateResourceTypes(resourceTypes);
if (validationFailures.length !== 0) {
const errorMessage = localize('resourceDeployment.FailedToLoadExtension', 'Failed to load extension: {0}, Error detected in the resource type definition in package.json, check debug console for details.', context.extensionPath);
vscode.window.showErrorMessage(errorMessage);
validationFailures.forEach(message => console.error(message));
return;
}
const openDialog = (resourceTypeName: string) => {
const filtered = resourceTypes.filter(resourceType => resourceType.name === resourceTypeName);
if (filtered.length !== 1) {
vscode.window.showErrorMessage(localize('resourceDeployment.UnknownResourceType', 'The resource type: {0} is not defined', resourceTypeName));
}
else {
let dialog = new ResourceDeploymentDialog(context, notebookService, toolsService, resourceTypeService, filtered[0]);
dialog.open();
}
};
vscode.commands.registerCommand('azdata.resource.sql-image.deploy', () => {
let dialog = new ResourceDeploymentDialog();
dialog.open();
openDialog('sql-image');
});
vscode.commands.registerCommand('azdata.resource.sql-bdc.deploy', () => {
let dialog = new ResourceDeploymentDialog();
dialog.open();
openDialog('sql-bdc');
});
}

View File

@@ -0,0 +1,79 @@
/*---------------------------------------------------------------------------------------------
* 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 { NotebookInfo } from '../interfaces';
import { isString } from 'util';
import * as os from 'os';
import * as path from 'path';
import * as nls from 'vscode-nls';
import { IPlatformService } from './platformService';
const localize = nls.loadMessageBundle();
export interface INotebookService {
launchNotebook(notebook: string | NotebookInfo): void;
}
export class NotebookService implements INotebookService {
constructor(private platformService: IPlatformService) { }
/**
* Copy the notebook to the user's home directory and launch the notebook from there.
* @param notebook the path of the notebook
*/
launchNotebook(notebook: string | NotebookInfo): void {
const notebookRelativePath = this.getNotebook(notebook);
const notebookFullPath = path.join(__dirname, '../../', notebookRelativePath);
if (notebookRelativePath && this.platformService.fileExists(notebookFullPath)) {
const targetFileName = this.getTargetNotebookFileName(notebookFullPath, os.homedir());
this.platformService.copyFile(notebookFullPath, targetFileName);
this.platformService.openFile(targetFileName);
}
else {
this.platformService.showErrorMessage(localize('resourceDeployment.notebookNotFound', 'The notebook {0} does not exist', notebookFullPath));
}
}
/**
* get the notebook path for current platform
* @param notebook the notebook path
*/
getNotebook(notebook: string | NotebookInfo): string {
let notebookPath;
if (notebook && !isString(notebook)) {
const platform = this.platformService.platform();
if (platform === 'win32') {
notebookPath = notebook.win32;
} else if (platform === 'darwin') {
notebookPath = notebook.darwin;
} else {
notebookPath = notebook.linux;
}
} else {
notebookPath = notebook;
}
return notebookPath;
}
/**
* Get a file name that is not already used in the target directory
* @param notebook source notebook file name
* @param targetDirectory target directory
*/
getTargetNotebookFileName(notebook: string, targetDirectory: string): string {
const notebookFileExtension = '.ipynb';
const baseName = path.basename(notebook, notebookFileExtension);
let targetFileName;
let idx = 0;
do {
const suffix = idx === 0 ? '' : `-${idx}`;
targetFileName = path.join(targetDirectory, `${baseName}${suffix}${notebookFileExtension}`);
idx++;
} while (this.platformService.fileExists(targetFileName));
return targetFileName;
}
}

View File

@@ -0,0 +1,41 @@
/*---------------------------------------------------------------------------------------------
* 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 fs from 'fs';
import * as vscode from 'vscode';
/**
* Abstract of platform dependencies
*/
export interface IPlatformService {
platform(): string;
copyFile(source: string, target: string): void;
fileExists(file: string): boolean;
openFile(filePath: string): void;
showErrorMessage(message: string): void;
}
export class PlatformService implements IPlatformService {
platform(): string {
return process.platform;
}
copyFile(source: string, target: string): void {
fs.copyFileSync(source, target);
}
fileExists(file: string): boolean {
return fs.existsSync(file);
}
openFile(filePath: string): void {
vscode.commands.executeCommand('vscode.open', vscode.Uri.file(filePath));
}
showErrorMessage(message: string): void {
vscode.window.showErrorMessage(message);
}
}

View File

@@ -0,0 +1,174 @@
/*---------------------------------------------------------------------------------------------
* 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 { ResourceType, ResourceTypeOption, DeploymentProvider } from '../interfaces';
import { IToolsService } from './toolsService';
import * as vscode from 'vscode';
import { IPlatformService } from './platformService';
export interface IResourceTypeService {
getResourceTypes(filterByPlatform?: boolean): ResourceType[];
validateResourceTypes(resourceTypes: ResourceType[]): string[];
}
export class ResourceTypeService implements IResourceTypeService {
private _resourceTypes: ResourceType[] = [];
constructor(private platformService: IPlatformService, private toolsService: IToolsService) { }
/**
* Get the supported resource types
* @param filterByPlatform indicates whether to return the resource types supported on current platform.
*/
getResourceTypes(filterByPlatform: boolean = true): ResourceType[] {
if (this._resourceTypes.length === 0) {
// If we load package.json directly using require(path) the contents won't be localized
this._resourceTypes = vscode.extensions.getExtension('microsoft.resource-deployment')!.packageJSON.resourceTypes as ResourceType[];
this._resourceTypes.forEach(resourceType => {
resourceType.getProvider = (selectedOptions) => { return this.getProvider(resourceType, selectedOptions); };
});
}
let resourceTypes = this._resourceTypes;
if (filterByPlatform) {
resourceTypes = resourceTypes.filter(resourceType => resourceType.platforms.includes(this.platformService.platform()));
}
return resourceTypes;
}
/**
* Validate the resource types and returns validation error messages if any.
* @param resourceTypes resource types to be validated
*/
validateResourceTypes(resourceTypes: ResourceType[]): string[] {
// NOTE: The validation error messages do not need to be localized as it is only meant for the developer's use.
const errorMessages: string[] = [];
if (!resourceTypes || resourceTypes.length === 0) {
errorMessages.push('Resource type list is empty');
} else {
let resourceTypeIndex = 1;
resourceTypes.forEach(resourceType => {
this.validateResourceType(resourceType, `resource type index: ${resourceTypeIndex}`, errorMessages);
resourceTypeIndex++;
});
}
return errorMessages;
}
private validateResourceType(resourceType: ResourceType, positionInfo: string, errorMessages: string[]): void {
this.validateNameDisplayName(resourceType, 'resource type', positionInfo, errorMessages);
if (!resourceType.icon || !resourceType.icon.dark || !resourceType.icon.light) {
errorMessages.push(`Icon for resource type is not specified properly. ${positionInfo} `);
}
if (resourceType.options && resourceType.options.length > 0) {
let optionIndex = 1;
resourceType.options.forEach(option => {
const optionInfo = `${positionInfo}, option index: ${optionIndex} `;
this.validateResourceTypeOption(option, optionInfo, errorMessages);
optionIndex++;
});
}
this.validateProviders(resourceType, positionInfo, errorMessages);
}
private validateResourceTypeOption(option: ResourceTypeOption, positionInfo: string, errorMessages: string[]): void {
this.validateNameDisplayName(option, 'option', positionInfo, errorMessages);
if (!option.values || option.values.length === 0) {
errorMessages.push(`Option contains no values.${positionInfo} `);
} else {
let optionValueIndex = 1;
option.values.forEach(optionValue => {
const optionValueInfo = `${positionInfo}, option value index: ${optionValueIndex} `;
this.validateNameDisplayName(optionValue, 'option value', optionValueInfo, errorMessages);
optionValueIndex++;
});
// Make sure the values are unique
for (let i = 0; i < option.values.length; i++) {
if (option.values[i].name && option.values[i].displayName) {
let dupePositions = [];
for (let j = i + 1; j < option.values.length; j++) {
if (option.values[i].name === option.values[j].name
|| option.values[i].displayName === option.values[j].displayName) {
// +1 to make the position 1 based.
dupePositions.push(j + 1);
}
}
if (dupePositions.length !== 0) {
errorMessages.push(`Option values with same name or display name are found at the following positions: ${i + 1}, ${dupePositions.join(',')}.${positionInfo} `);
}
}
}
}
}
private validateProviders(resourceType: ResourceType, positionInfo: string, errorMessages: string[]): void {
if (!resourceType.providers || resourceType.providers.length === 0) {
errorMessages.push(`No providers defined for resource type, ${positionInfo}`);
} else {
let providerIndex = 1;
resourceType.providers.forEach(provider => {
const providerPositionInfo = `${positionInfo}, provider index: ${providerIndex} `;
if (!provider.notebook) {
errorMessages.push(`Notebook is not specified for the provider, ${providerPositionInfo}`);
}
if (provider.requiredTools && provider.requiredTools.length > 0) {
provider.requiredTools.forEach(tool => {
if (!this.toolsService.getToolByName(tool.name)) {
errorMessages.push(`The tool is not supported: ${tool.name}, ${providerPositionInfo} `);
}
});
}
providerIndex++;
});
}
}
private validateNameDisplayName(obj: { name: string; displayName: string }, type: string, positionInfo: string, errorMessages: string[]): void {
if (!obj.name) {
errorMessages.push(`Name of the ${type} is empty.${positionInfo} `);
}
if (!obj.displayName) {
errorMessages.push(`Display name of the ${type} is empty.${positionInfo} `);
}
}
/**
* Get the provider based on the selected options
*/
private getProvider(resourceType: ResourceType, selectedOptions: { option: string, value: string }[]): DeploymentProvider | undefined {
for (let i = 0; i < resourceType.providers.length; i++) {
const provider = resourceType.providers[i];
const expected = provider.when.replace(' ', '').split('&&').sort();
let actual: string[] = [];
selectedOptions.forEach(option => {
actual.push(`${option.option}=${option.value}`);
});
actual = actual.sort();
if (actual.length === expected.length) {
let matches = true;
for (let j = 0; j < actual.length; j++) {
if (actual[j] !== expected[j]) {
matches = false;
break;
}
}
if (matches) {
return provider;
}
}
}
return undefined;
}
}

View File

@@ -0,0 +1,43 @@
/*---------------------------------------------------------------------------------------------
* 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 { ToolType, ITool, ToolInstallationStatus } from '../../interfaces';
import * as nls from 'vscode-nls';
const localize = nls.loadMessageBundle();
export class AzCliTool implements ITool {
get name(): string {
return 'azcli';
}
get description(): string {
return localize('resourceDeployment.AzCLIDescription', 'Tool used for managing Azure services');
}
get type(): ToolType {
return ToolType.AzCli;
}
get displayName(): string {
return localize('resourceDeployment.AzCLIDisplayName', 'Azure CLI');
}
get supportAutoInstall(): boolean {
return true;
}
install(version: string): Thenable<void> {
throw new Error('Method not implemented.');
}
getInstallationStatus(versionExpression: string): Thenable<ToolInstallationStatus> {
let promise = new Promise<ToolInstallationStatus>(resolve => {
setTimeout(() => {
resolve(ToolInstallationStatus.Installed);
}, 500);
});
return promise;
}
}

View File

@@ -0,0 +1,44 @@
/*---------------------------------------------------------------------------------------------
* 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 { ToolType, ITool, ToolInstallationStatus } from '../../interfaces';
import * as nls from 'vscode-nls';
const localize = nls.loadMessageBundle();
export class DockerTool implements ITool {
get name(): string {
return 'docker';
}
get description(): string {
return localize('resourceDeployment.DockerDescription', 'Manages the containers');
}
get type(): ToolType {
return ToolType.Docker;
}
get displayName(): string {
return localize('resourceDeployment.DockerDisplayName', 'Docker');
}
get supportAutoInstall(): boolean {
return true;
}
install(version: string): Thenable<void> {
throw new Error('Method not implemented.');
}
getInstallationStatus(versionExpression: string): Thenable<ToolInstallationStatus> {
let promise = new Promise<ToolInstallationStatus>(resolve => {
setTimeout(() => {
resolve(ToolInstallationStatus.Installed);
}, 500);
});
return promise;
}
}

View File

@@ -0,0 +1,43 @@
/*---------------------------------------------------------------------------------------------
* 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 { ToolType, ITool, ToolInstallationStatus } from '../../interfaces';
import * as nls from 'vscode-nls';
const localize = nls.loadMessageBundle();
export class KubeCtlTool implements ITool {
get name(): string {
return 'kubectl';
}
get description(): string {
return localize('resourceDeployment.KUBECTLDescription', 'Tool used for managing the Kubernetes cluster');
}
get type(): ToolType {
return ToolType.KubeCtl;
}
get displayName(): string {
return localize('resourceDeployment.KUBECTLDisplayName', 'kubectl');
}
get supportAutoInstall(): boolean {
return true;
}
install(version: string): Thenable<void> {
throw new Error('Method not implemented.');
}
getInstallationStatus(versionExpression: string): Thenable<ToolInstallationStatus> {
let promise = new Promise<ToolInstallationStatus>(resolve => {
setTimeout(() => {
resolve(ToolInstallationStatus.Installed);
}, 500);
});
return promise;
}
}

View File

@@ -0,0 +1,52 @@
/*---------------------------------------------------------------------------------------------
* 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 { ToolType, ITool, ToolInstallationStatus } from '../../interfaces';
import * as nls from 'vscode-nls';
const localize = nls.loadMessageBundle();
export class MSSQLCtlTool implements ITool {
get name(): string {
return 'mssqlctl';
}
get description(): string {
return localize('resourceDeployment.MSSQLCTLDescription', 'Command-line tool for installing and managing the SQL Server big data cluster');
}
get type(): ToolType {
return ToolType.MSSQLCtl;
}
get displayName(): string {
return localize('resourceDeployment.MSSQLCTLDisplayName', 'mssqlctl');
}
isInstalled(versionExpression: string): Thenable<boolean> {
let promise = new Promise<boolean>(resolve => {
setTimeout(() => {
resolve(true);
}, 500);
});
return promise;
}
get supportAutoInstall(): boolean {
return true;
}
install(version: string): Thenable<void> {
throw new Error('Method not implemented.');
}
getInstallationStatus(versionExpression: string): Thenable<ToolInstallationStatus> {
let promise = new Promise<ToolInstallationStatus>(resolve => {
setTimeout(() => {
resolve(ToolInstallationStatus.Installed);
}, 500);
});
return promise;
}
}

View File

@@ -0,0 +1,44 @@
/*---------------------------------------------------------------------------------------------
* 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 { ToolType, ITool, ToolInstallationStatus } from '../../interfaces';
import * as nls from 'vscode-nls';
const localize = nls.loadMessageBundle();
export class PythonTool implements ITool {
get name(): string {
return 'python';
}
get description(): string {
return localize('resourceDeployment.PythonDescription', 'Required by notebook feature');
}
get type(): ToolType {
return ToolType.Python;
}
get displayName(): string {
return localize('resourceDeployment.PythonDisplayName', 'Python');
}
get supportAutoInstall(): boolean {
return true;
}
install(version: string): Thenable<void> {
throw new Error('Method not implemented.');
}
getInstallationStatus(versionExpression: string): Thenable<ToolInstallationStatus> {
let promise = new Promise<ToolInstallationStatus>(resolve => {
setTimeout(() => {
resolve(ToolInstallationStatus.Installed);
}, 500);
});
return promise;
}
}

View File

@@ -0,0 +1,52 @@
/*---------------------------------------------------------------------------------------------
* 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 { ToolRequirementInfo, ToolStatusInfo, ITool } from '../interfaces';
import { PythonTool } from './tools/pythonTool';
import { DockerTool } from './tools/dockerTool';
import { AzCliTool } from './tools/azCliTool';
import { MSSQLCtlTool } from './tools/mssqlCtlTool';
import { KubeCtlTool } from './tools/kubeCtlTool';
export interface IToolsService {
getToolStatus(toolRequirements: ToolRequirementInfo[]): Thenable<ToolStatusInfo[]>;
getToolByName(toolName: string): ITool | undefined;
}
export class ToolsService implements IToolsService {
private static readonly SupportedTools: ITool[] = [new PythonTool(), new DockerTool(), new AzCliTool(), new MSSQLCtlTool(), new KubeCtlTool()];
getToolStatus(toolRequirements: ToolRequirementInfo[]): Thenable<ToolStatusInfo[]> {
const toolStatusList: ToolStatusInfo[] = [];
let promises = [];
for (let i = 0; i < toolRequirements.length; i++) {
const toolRequirement = toolRequirements[i];
const tool = this.getToolByName(toolRequirement.name);
if (tool !== undefined) {
promises.push(tool.getInstallationStatus(toolRequirement.version).then(installStatus => {
toolStatusList.push(<ToolStatusInfo>{
name: tool.displayName,
description: tool.description,
status: installStatus,
version: toolRequirement.version
});
}));
}
}
return Promise.all(promises).then(() => { return toolStatusList; });
}
getToolByName(toolName: string): ITool | undefined {
if (toolName) {
for (let i = 0; i < ToolsService.SupportedTools.length; i++) {
if (toolName === ToolsService.SupportedTools[i].name) {
return ToolsService.SupportedTools[i];
}
}
}
return undefined;
}
}

View File

@@ -0,0 +1,30 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
const path = require('path');
const testRunner = require('vscode/lib/testrunner');
const suite = 'Resource Deployment Unit Tests';
const testOptions: any = {
ui: 'tdd',
useColors: true,
timeout: 60000
};
if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY) {
testOptions.reporter = 'mocha-multi-reporters';
testOptions.reporterOptions = {
reporterEnabled: 'spec, mocha-junit-reporter',
mochaJunitReporterReporterOptions: {
testsuitesTitle: `${suite} ${process.platform}`,
mochaFile: path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`)
}
};
}
testRunner.configure(testOptions);
export = testRunner;

View File

@@ -0,0 +1,130 @@
/*---------------------------------------------------------------------------------------------
* 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 TypeMoq from 'typemoq';
import 'mocha';
import * as path from 'path';
import * as os from 'os';
import { NotebookService } from '../services/NotebookService';
import assert = require('assert');
import { NotebookInfo } from '../interfaces';
import { IPlatformService } from '../services/platformService';
suite('Notebook Service Tests', function (): void {
test('getNotebook with string parameter', () => {
const mockPlatformService = TypeMoq.Mock.ofType<IPlatformService>();
const notebookService = new NotebookService(mockPlatformService.object);
const notebookInput = 'test-notebook.ipynb';
mockPlatformService.setup((service) => service.platform()).returns(() => { return 'win32'; });
let returnValue = notebookService.getNotebook(notebookInput);
assert.equal(returnValue, notebookInput, 'returned notebook name does not match expected value');
mockPlatformService.verify((service) => service.platform(), TypeMoq.Times.never());
mockPlatformService.reset();
mockPlatformService.setup((service) => service.platform()).returns(() => { return 'win32'; });
returnValue = notebookService.getNotebook('');
assert.equal(returnValue, '', 'returned notebook name does not match expected value is not an empty string');
mockPlatformService.verify((service) => service.platform(), TypeMoq.Times.never());
});
test('getNotebook with NotebookInfo parameter', () => {
const mockPlatformService = TypeMoq.Mock.ofType<IPlatformService>();
const notebookService = new NotebookService(mockPlatformService.object);
const notebookWin32 = 'test-notebook-win32.ipynb';
const notebookDarwin = 'test-notebook-darwin.ipynb';
const notebookLinux = 'test-notebook-linux.ipynb';
const notebookInput: NotebookInfo = {
darwin: notebookDarwin,
win32: notebookWin32,
linux: notebookLinux
};
mockPlatformService.setup((service) => service.platform()).returns(() => { return 'win32'; });
let returnValue = notebookService.getNotebook(notebookInput);
assert.equal(returnValue, notebookWin32, 'returned notebook name does not match expected value for win32 platform');
mockPlatformService.verify((service) => service.platform(), TypeMoq.Times.once());
mockPlatformService.reset();
mockPlatformService.setup((service) => service.platform()).returns(() => { return 'darwin'; });
returnValue = notebookService.getNotebook(notebookInput);
assert.equal(returnValue, notebookDarwin, 'returned notebook name does not match expected value for darwin platform');
mockPlatformService.verify((service) => service.platform(), TypeMoq.Times.once());
mockPlatformService.reset();
mockPlatformService.setup((service) => service.platform()).returns(() => { return 'linux'; });
returnValue = notebookService.getNotebook(notebookInput);
assert.equal(returnValue, notebookLinux, 'returned notebook name does not match expected value for linux platform');
mockPlatformService.verify((service) => service.platform(), TypeMoq.Times.once());
});
test('launchNotebook', () => {
const mockPlatformService = TypeMoq.Mock.ofType<IPlatformService>();
const notebookService = new NotebookService(mockPlatformService.object);
const notebookFileName = 'mynotebook.ipynb';
const notebookPath = `./notebooks/${notebookFileName}`;
let actualSourceFile;
const expectedSourceFile = path.join(__dirname, '../../', notebookPath);
let actualTargetFile;
const expectedTargetFile = path.join(os.homedir(), notebookFileName);
mockPlatformService.setup((service) => service.platform()).returns(() => { return 'win32'; });
mockPlatformService.setup((service) => service.openFile(TypeMoq.It.isAnyString()));
mockPlatformService.setup((service) => service.fileExists(TypeMoq.It.isAnyString()))
.returns((path) => {
if (path === expectedSourceFile) {
return true;
}
return false;
});
mockPlatformService.setup((service) => service.copyFile(TypeMoq.It.isAnyString(), TypeMoq.It.isAnyString()))
.returns((source, target) => { actualSourceFile = source; actualTargetFile = target; });
notebookService.launchNotebook(notebookPath);
mockPlatformService.verify((service) => service.copyFile(TypeMoq.It.isAnyString(), TypeMoq.It.isAnyString()), TypeMoq.Times.once());
mockPlatformService.verify((service) => service.openFile(TypeMoq.It.isAnyString()), TypeMoq.Times.once());
assert.equal(actualSourceFile, expectedSourceFile, 'source file is not correct');
assert.equal(actualTargetFile, expectedTargetFile, 'target file is not correct');
});
test('getTargetNotebookFileName with no name conflict', () => {
const mockPlatformService = TypeMoq.Mock.ofType<IPlatformService>();
const notebookService = new NotebookService(mockPlatformService.object);
const notebookFileName = 'mynotebook.ipynb';
const sourceNotebookPath = `./notebooks/${notebookFileName}`;
const expectedTargetFile = path.join(os.homedir(), notebookFileName);
mockPlatformService.setup((service) => service.fileExists(TypeMoq.It.isAnyString()))
.returns((path) => { return false; });
const actualFileName = notebookService.getTargetNotebookFileName(sourceNotebookPath, os.homedir());
mockPlatformService.verify((service) => service.fileExists(TypeMoq.It.isAnyString()), TypeMoq.Times.once());
assert.equal(actualFileName, expectedTargetFile, 'target file name is not correct');
});
test('getTargetNotebookFileName with name conflicts', () => {
const mockPlatformService = TypeMoq.Mock.ofType<IPlatformService>();
const notebookService = new NotebookService(mockPlatformService.object);
const notebookFileName = 'mynotebook.ipynb';
const sourceNotebookPath = `./notebooks/${notebookFileName}`;
const expectedFileName = 'mynotebook-2.ipynb';
const expected1stAttemptTargetFile = path.join(os.homedir(), notebookFileName);
const expected2ndAttemptTargetFile = path.join(os.homedir(), 'mynotebook-1.ipynb');
const expectedTargetFile = path.join(os.homedir(), expectedFileName);
mockPlatformService.setup((service) => service.fileExists(TypeMoq.It.isAnyString()))
.returns((path) => {
// list all the possible values here and handle them
// if we only handle the expected value and return true for anything else, the test might run forever until times out
if (path === expected1stAttemptTargetFile || path === expected2ndAttemptTargetFile) {
return true;
}
return false;
});
const actualFileName = notebookService.getTargetNotebookFileName(sourceNotebookPath, os.homedir());
mockPlatformService.verify((service) => service.fileExists(TypeMoq.It.isAnyString()), TypeMoq.Times.exactly(3));
assert.equal(actualFileName, expectedTargetFile, 'target file name is not correct');
});
});

View File

@@ -6,26 +6,210 @@
import * as azdata from 'azdata';
import * as nls from 'vscode-nls';
import { IResourceTypeService } from '../services/resourceTypeService';
import * as vscode from 'vscode';
import { ResourceType, DeploymentProvider, ToolInstallationStatus } from '../interfaces';
import { IToolsService } from '../services/toolsService';
import { INotebookService } from '../services/notebookService';
const localize = nls.loadMessageBundle();
export class ResourceDeploymentDialog {
private dialogObject: azdata.window.Dialog;
private _selectedResourceType: ResourceType;
private _toDispose: vscode.Disposable[] = [];
private _dialogObject: azdata.window.Dialog;
private _resourceTypeCards: azdata.CardComponent[] = [];
private _view!: azdata.ModelView;
private _resourceDescriptionLabel!: azdata.TextComponent;
private _optionsContainer!: azdata.FlexContainer;
private _toolsTable!: azdata.TableComponent;
private _cardResourceTypeMap: Map<string, azdata.CardComponent> = new Map();
private _optionDropDownMap: Map<string, azdata.DropDownComponent> = new Map();
constructor() {
this.dialogObject = azdata.window.createModelViewDialog(localize('deploymentDialog.title', 'Install SQL Server'), 'resourceDeploymentDialog', true);
constructor(private context: vscode.ExtensionContext,
private notebookService: INotebookService,
private toolsService: IToolsService,
private resourceTypeService: IResourceTypeService,
resourceType: ResourceType) {
this._selectedResourceType = resourceType;
this._dialogObject = azdata.window.createModelViewDialog(localize('deploymentDialog.title', 'Select a configuration'), 'resourceDeploymentDialog', true);
this._dialogObject.cancelButton.onClick(() => this.onCancel());
this._dialogObject.okButton.label = localize('deploymentDialog.OKButtonText', 'Select');
this._dialogObject.okButton.onClick(() => this.onComplete());
}
private initializeDialog() {
let tab = azdata.window.createTab('');
tab.registerContent((view: azdata.ModelView) => {
let text = view.modelBuilder.text().withProperties<azdata.TextComponentProperties>({ value: 'place holder' }).component();
return view.initializeModel(text);
this._view = view;
this.resourceTypeService.getResourceTypes().forEach(resourceType => this.addCard(resourceType));
const cardsContainer = view.modelBuilder.flexContainer().withItems(this._resourceTypeCards, { flex: '0 0 auto', CSSStyles: { 'margin-bottom': '10px' } }).withLayout({ flexFlow: 'row', alignItems: 'left' }).component();
this._resourceDescriptionLabel = view.modelBuilder.text().withProperties<azdata.TextComponentProperties>({ value: this._selectedResourceType ? this._selectedResourceType.description : undefined }).component();
this._optionsContainer = view.modelBuilder.flexContainer().withLayout({ flexFlow: 'column' }).component();
const toolColumn: azdata.TableColumn = {
value: localize('deploymentDialog.toolNameColumnHeader', 'Tool'),
width: 100
};
const descriptionColumn: azdata.TableColumn = {
value: localize('deploymentDialog.toolDescriptionColumnHeader', 'Description'),
width: 500
};
const versionColumn: azdata.TableColumn = {
value: localize('deploymentDialog.toolVersionColumnHeader', 'Version'),
width: 200
};
const statusColumn: azdata.TableColumn = {
value: localize('deploymentDialog.toolStatusColumnHeader', 'Status'),
width: 200
};
this._toolsTable = view.modelBuilder.table().withProperties<azdata.TableComponentProperties>({
height: 150,
data: [],
columns: [toolColumn, descriptionColumn, versionColumn, statusColumn],
width: 1000
}).component();
const formBuilder = view.modelBuilder.formContainer().withFormItems(
[
{
component: cardsContainer,
title: ''
}, {
component: this._resourceDescriptionLabel,
title: ''
}, {
component: this._optionsContainer,
title: localize('deploymentDialog.OptionsTitle', 'Options')
}, {
component: this._toolsTable,
title: localize('deploymentDialog.RequiredToolsTitle', 'Required tools')
}
],
{
horizontal: false
}
);
const form = formBuilder.withLayout({ width: '100%' }).component();
if (this._selectedResourceType) {
this.selectResourceType(this._selectedResourceType);
}
return view.initializeModel(form);
});
this.dialogObject.content = [tab];
this._dialogObject.content = [tab];
}
public open(): void {
this.initializeDialog();
azdata.window.openDialog(this.dialogObject);
azdata.window.openDialog(this._dialogObject);
}
private addCard(resourceType: ResourceType): void {
const card = this._view.modelBuilder.card().withProperties<azdata.CardProperties>({
cardType: azdata.CardType.VerticalButton,
iconPath: {
dark: this.context.asAbsolutePath(resourceType.icon.dark),
light: this.context.asAbsolutePath(resourceType.icon.light)
},
label: resourceType.displayName,
selected: (this._selectedResourceType && this._selectedResourceType.name === resourceType.name)
}).component();
this._resourceTypeCards.push(card);
this._cardResourceTypeMap.set(resourceType.name, card);
this._toDispose.push(card.onCardSelectedChanged(() => this.selectResourceType(resourceType)));
}
private selectResourceType(resourceType: ResourceType): void {
this._selectedResourceType = resourceType;
const card = this._cardResourceTypeMap.get(this._selectedResourceType.name)!;
if (card.selected) {
// clear the selected state of the previously selected card
this._resourceTypeCards.forEach(c => {
if (c !== card) {
c.selected = false;
}
});
} else {
// keep the selected state if no other card is selected
if (this._resourceTypeCards.filter(c => { return c !== card && c.selected; }).length === 0) {
card.selected = true;
}
}
this._resourceDescriptionLabel.value = resourceType.description;
this._optionsContainer.clearItems();
this._optionDropDownMap.clear();
resourceType.options.forEach(option => {
const optionLabel = this._view.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
value: option.displayName
}).component();
optionLabel.width = '150px';
const optionSelectBox = this._view.modelBuilder.dropDown().withProperties<azdata.DropDownProperties>({
values: option.values,
value: option.values[0],
width: '300px'
}).component();
this._toDispose.push(optionSelectBox.onValueChanged(() => { this.updateTools(); }));
this._optionDropDownMap.set(option.name, optionSelectBox);
const row = this._view.modelBuilder.flexContainer().withItems([optionLabel, optionSelectBox], { flex: '0 0 auto', CSSStyles: { 'margin-right': '20px' } }).withLayout({ flexFlow: 'row', alignItems: 'center' }).component();
this._optionsContainer.addItem(row);
});
this.updateTools();
}
private updateTools(): void {
this.toolsService.getToolStatus(this.getCurrentProvider().requiredTools).then(toolStatus => {
let tableData = toolStatus.map(tool => {
return [tool.name, tool.description, tool.version, this.getToolStatusText(tool.status)];
});
this._toolsTable.data = tableData;
});
}
private getToolStatusText(status: ToolInstallationStatus): string {
switch (status) {
case ToolInstallationStatus.Installed:
return '✔️ ' + localize('deploymentDialog.InstalledText', 'Installed');
case ToolInstallationStatus.NotInstalled:
return '❌ ' + localize('deploymentDialog.NotInstalledText', 'Not Installed');
case ToolInstallationStatus.Installing:
return '⌛ ' + localize('deploymentDialog.InstallingText', 'Installing…');
case ToolInstallationStatus.FailedToInstall:
return '❌ ' + localize('deploymentDialog.FailedToInstallText', 'Install Failed');
default:
return 'unknown status';
}
}
private getCurrentProvider(): DeploymentProvider {
const options: { option: string, value: string }[] = [];
this._optionDropDownMap.forEach((selectBox, option) => {
let selectedValue: azdata.CategoryValue = selectBox.value as azdata.CategoryValue;
options.push({ option: option, value: selectedValue.name });
});
return this._selectedResourceType.getProvider(options)!;
}
private onCancel(): void {
this.dispose();
}
private onComplete(): void {
const provider = this.getCurrentProvider();
this.notebookService.launchNotebook(provider.notebook);
this.dispose();
}
private dispose(): void {
this._toDispose.forEach(disposable => disposable.dispose());
}
}