Add datavirtualization extension (#21594)

* initial

* cleanup

* Add typings ref

* fix compile

* remove unused

* add missing

* another unused

* Use newer vscodetestcover

* newer dataprotocol

* format

* cleanup ignores

* fix out path

* fix entry point

* more cleanup

* Move into src folder

* Handle service client log messages

* remove unused
This commit is contained in:
Charles Gagnon
2023-01-17 09:57:21 -08:00
committed by GitHub
parent 9184c414de
commit ec838947b0
103 changed files with 12432 additions and 1 deletions

View File

@@ -0,0 +1,202 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import * as azdata from 'azdata';
import * as constants from './constants';
/**
* Wrapper class to act as a facade over VSCode and Data APIs and allow us to test / mock callbacks into
* this API from our code
*
* @export
* @class ApiWrapper
*/
export class ApiWrapper {
// Data APIs
public registerConnectionProvider(provider: azdata.ConnectionProvider): vscode.Disposable {
return azdata.dataprotocol.registerConnectionProvider(provider);
}
public registerObjectExplorerProvider(provider: azdata.ObjectExplorerProvider): vscode.Disposable {
return azdata.dataprotocol.registerObjectExplorerProvider(provider);
}
public registerTaskServicesProvider(provider: azdata.TaskServicesProvider): vscode.Disposable {
return azdata.dataprotocol.registerTaskServicesProvider(provider);
}
public registerFileBrowserProvider(provider: azdata.FileBrowserProvider): vscode.Disposable {
return azdata.dataprotocol.registerFileBrowserProvider(provider);
}
public registerCapabilitiesServiceProvider(provider: azdata.CapabilitiesProvider): vscode.Disposable {
return azdata.dataprotocol.registerCapabilitiesServiceProvider(provider);
}
public registerModelViewProvider(widgetId: string, handler: (modelView: azdata.ModelView) => void): void {
return azdata.ui.registerModelViewProvider(widgetId, handler);
}
public registerWebviewProvider(widgetId: string, handler: (webview: azdata.DashboardWebview) => void): void {
return azdata.dashboard.registerWebviewProvider(widgetId, handler);
}
public createDialog(title: string): azdata.window.Dialog {
return azdata.window.createModelViewDialog(title);
}
public openDialog(dialog: azdata.window.Dialog): void {
return azdata.window.openDialog(dialog);
}
public closeDialog(dialog: azdata.window.Dialog): void {
return azdata.window.closeDialog(dialog);
}
public registerTaskHandler(taskId: string, handler: (profile: azdata.IConnectionProfile) => void): void {
azdata.tasks.registerTask(taskId, handler);
}
public startBackgroundOperation(operationInfo: azdata.BackgroundOperationInfo): void {
azdata.tasks.startBackgroundOperation(operationInfo);
}
public getActiveConnections(): Thenable<azdata.connection.Connection[]> {
return azdata.connection.getActiveConnections();
}
public getCurrentConnection(): Thenable<azdata.connection.ConnectionProfile> {
return azdata.connection.getCurrentConnection();
}
public createModelViewEditor(title: string, options?: azdata.ModelViewEditorOptions): azdata.workspace.ModelViewEditor {
return azdata.workspace.createModelViewEditor(title, options);
}
// VSCode APIs
public createTerminal(name?: string, shellPath?: string, shellArgs?: string[]): vscode.Terminal {
return vscode.window.createTerminal(name, shellPath, shellArgs);
}
public createTerminalWithOptions(options: vscode.TerminalOptions): vscode.Terminal {
return vscode.window.createTerminal(options);
}
public executeCommand(command: string, ...rest: any[]): Thenable<any> {
return vscode.commands.executeCommand(command, ...rest);
}
public getFilePathRelativeToWorkspace(uri: vscode.Uri): string {
return vscode.workspace.asRelativePath(uri);
}
public getWorkspacePathFromUri(uri: vscode.Uri): string | undefined {
let workspaceFolder = vscode.workspace.getWorkspaceFolder(uri);
return workspaceFolder ? workspaceFolder.uri.fsPath : undefined;
}
public registerCommand(command: string, callback: (...args: any[]) => any, thisArg?: any): vscode.Disposable {
return vscode.commands.registerCommand(command, callback, thisArg);
}
public registerDocumentOpenHandler(handler: (doc: vscode.TextDocument) => any): vscode.Disposable {
return vscode.workspace.onDidOpenTextDocument(handler);
}
public registerTreeDataProvider<T>(viewId: string, treeDataProvider: vscode.TreeDataProvider<T>): vscode.Disposable {
return vscode.window.registerTreeDataProvider(viewId, treeDataProvider);
}
public setCommandContext(key: constants.CommandContext | string, value: any): Thenable<any> {
return vscode.commands.executeCommand(constants.BuiltInCommands.SetContext, key, value);
}
/**
* Get the configuration for a extensionName
* @param extensionName The string name of the extension to get the configuration for
* @param resource The optional URI, as a URI object or a string, to use to get resource-scoped configurations
*/
public getConfiguration(extensionName?: string, resource?: vscode.Uri | string): vscode.WorkspaceConfiguration {
if (typeof resource === 'string') {
try {
resource = this.parseUri(resource);
} catch (e) {
resource = undefined;
}
} else if (!resource) {
// Fix to avoid adding lots of errors to debug console. Expects a valid resource or null, not undefined
resource = null;
}
return vscode.workspace.getConfiguration(extensionName, resource as vscode.Uri);
}
public getExtensionConfiguration(): vscode.WorkspaceConfiguration {
return this.getConfiguration(constants.extensionConfigSectionName);
}
/**
* Parse uri
*/
public parseUri(uri: string): vscode.Uri {
return vscode.Uri.parse(uri);
}
public showOpenDialog(options: vscode.OpenDialogOptions): Thenable<vscode.Uri[] | undefined> {
return vscode.window.showOpenDialog(options);
}
public showSaveDialog(options: vscode.SaveDialogOptions): Thenable<vscode.Uri> {
return vscode.window.showSaveDialog(options);
}
public openTextDocument(uri: vscode.Uri): Thenable<vscode.TextDocument>;
public openTextDocument(options: { language?: string; content?: string; }): Thenable<vscode.TextDocument>;
public openTextDocument(uriOrOptions): Thenable<vscode.TextDocument> {
return vscode.workspace.openTextDocument(uriOrOptions);
}
public showDocument(document: vscode.TextDocument): Thenable<vscode.TextEditor> {
return this.executeCommand('vscode.open', document.uri);
}
public showErrorMessage(message: string, ...items: string[]): Thenable<string | undefined> {
return vscode.window.showErrorMessage(message, ...items);
}
public showWarningMessage(message: string, ...items: string[]): Thenable<string | undefined> {
return vscode.window.showWarningMessage(message, ...items);
}
public showInformationMessage(message: string, ...items: string[]): Thenable<string | undefined> {
return vscode.window.showInformationMessage(message, ...items);
}
public createStatusBarItem(alignment?: vscode.StatusBarAlignment, priority?: number): vscode.StatusBarItem {
return vscode.window.createStatusBarItem(alignment, priority);
}
public createOutputChannel(name: string): vscode.OutputChannel {
return vscode.window.createOutputChannel(name);
}
public createWizardPage(title: string): azdata.window.WizardPage {
return azdata.window.createWizardPage(title);
}
public registerCompletionItemProvider(selector: vscode.DocumentSelector, provider: vscode.CompletionItemProvider, ...triggerCharacters: string[]): vscode.Disposable {
return vscode.languages.registerCompletionItemProvider(selector, provider, ...triggerCharacters);
}
public createTab(title: string): azdata.window.DialogTab {
return azdata.window.createTab(title);
}
// Connection APIs
public openConnectionDialog(providers: string[], initialConnectionProfile?: azdata.IConnectionProfile, connectionCompletionOptions?: azdata.IConnectionCompletionOptions): Thenable<azdata.connection.Connection> {
return azdata.connection.openConnectionDialog(providers, initialConnectionProfile, connectionCompletionOptions);
}
}

View File

@@ -0,0 +1,26 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import { ApiWrapper } from './apiWrapper';
/**
* Global context for the application
*/
export class AppContext {
private serviceMap: Map<string, any> = new Map();
constructor(public readonly extensionContext: vscode.ExtensionContext, public readonly apiWrapper: ApiWrapper) {
this.apiWrapper = apiWrapper || new ApiWrapper();
}
public getService<T>(serviceName: string): T {
return this.serviceMap.get(serviceName) as T;
}
public registerService<T>(serviceName: string, service: T): void {
this.serviceMap.set(serviceName, service);
}
}

View File

@@ -0,0 +1,25 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Transform } from 'stream';
import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
const localize = nls.loadMessageBundle();
export class CancelableStream extends Transform {
constructor(private cancelationToken: vscode.CancellationTokenSource) {
super();
}
public _transform(chunk: any, encoding: string, callback: Function): void {
if (this.cancelationToken && this.cancelationToken.token.isCancellationRequested) {
callback(new Error(localize('streamCanceled', 'Stream operation canceled by the user')));
} else {
this.push(chunk);
callback();
}
}
}

View File

@@ -0,0 +1,175 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import * as azdata from 'azdata';
import * as nls from 'vscode-nls';
const localize = nls.loadMessageBundle();
import { ApiWrapper } from './apiWrapper';
import { TreeNode } from './treeNodes';
import { QuestionTypes, IPrompter, IQuestion } from './prompts/question';
import * as utils from './utils';
import * as constants from './constants';
import { AppContext } from './appContext';
export interface ICommandContextParsingOptions {
editor: boolean;
uri: boolean;
}
export interface ICommandBaseContext {
command: string;
editor?: vscode.TextEditor;
uri?: vscode.Uri;
}
export interface ICommandUnknownContext extends ICommandBaseContext {
type: 'unknown';
}
export interface ICommandUriContext extends ICommandBaseContext {
type: 'uri';
}
export interface ICommandViewContext extends ICommandBaseContext {
type: 'view';
node: TreeNode;
}
export interface ICommandObjectExplorerContext extends ICommandBaseContext {
type: 'objectexplorer';
explorerContext: azdata.ObjectExplorerContext;
}
export type CommandContext = ICommandObjectExplorerContext | ICommandViewContext | ICommandUriContext | ICommandUnknownContext;
function isTextEditor(editor: any): editor is vscode.TextEditor {
if (editor === undefined) { return false; }
return editor.id !== undefined && ((editor as vscode.TextEditor).edit !== undefined || (editor as vscode.TextEditor).document !== undefined);
}
export abstract class Command extends vscode.Disposable {
protected readonly contextParsingOptions: ICommandContextParsingOptions = { editor: false, uri: false };
private disposable: vscode.Disposable;
constructor(command: string | string[], protected appContext: AppContext) {
super(() => this.dispose());
if (typeof command === 'string') {
this.disposable = this.apiWrapper.registerCommand(command, (...args: any[]) => this._execute(command, ...args), this);
return;
}
const subscriptions = command.map(cmd => this.apiWrapper.registerCommand(cmd, (...args: any[]) => this._execute(cmd, ...args), this));
this.disposable = vscode.Disposable.from(...subscriptions);
}
dispose(): void {
this.disposable && this.disposable.dispose();
}
protected get apiWrapper(): ApiWrapper {
return this.appContext.apiWrapper;
}
protected async preExecute(context: CommandContext, ...args: any[]): Promise<any> {
return this.execute(...args);
}
abstract execute(...args: any[]): any;
protected _execute(command: string, ...args: any[]): any {
// TODO consider using Telemetry.trackEvent(command);
const [context, rest] = Command.parseContext(command, this.contextParsingOptions, ...args);
return this.preExecute(context, ...rest);
}
private static parseContext(command: string, options: ICommandContextParsingOptions, ...args: any[]): [CommandContext, any[]] {
let editor: vscode.TextEditor | undefined = undefined;
let firstArg = args[0];
if (options.editor && (firstArg === undefined || isTextEditor(firstArg))) {
editor = firstArg;
args = args.slice(1);
firstArg = args[0];
}
if (options.uri && (firstArg === undefined || firstArg instanceof vscode.Uri)) {
const [uri, ...rest] = args as [vscode.Uri, any];
return [{ command: command, type: 'uri', editor: editor, uri: uri }, rest];
}
if (firstArg instanceof TreeNode) {
const [node, ...rest] = args as [TreeNode, any];
return [{ command: command, type: constants.ViewType, node: node }, rest];
}
if (firstArg && utils.isObjectExplorerContext(firstArg)) {
const [explorerContext, ...rest] = args as [azdata.ObjectExplorerContext, any];
return [{ command: command, type: constants.ObjectExplorerService, explorerContext: explorerContext }, rest];
}
return [{ command: command, type: 'unknown', editor: editor }, args];
}
}
export abstract class ProgressCommand extends Command {
static progressId = 0;
constructor(private command: string, protected prompter: IPrompter, appContext: AppContext) {
super(command, appContext);
}
protected async executeWithProgress(
execution: (cancelToken: vscode.CancellationTokenSource) => Promise<void>,
label: string,
isCancelable: boolean = false,
onCanceled?: () => void
): Promise<void> {
let disposables: vscode.Disposable[] = [];
const tokenSource = new vscode.CancellationTokenSource();
const statusBarItem = this.apiWrapper.createStatusBarItem(vscode.StatusBarAlignment.Left);
disposables.push(vscode.Disposable.from(statusBarItem));
statusBarItem.text = localize('progress', '$(sync~spin) {0}...', label);
if (isCancelable) {
const cancelCommandId = `cancelProgress${ProgressCommand.progressId++}`;
disposables.push(this.apiWrapper.registerCommand(cancelCommandId, async () => {
if (await this.confirmCancel()) {
tokenSource.cancel();
}
}));
statusBarItem.tooltip = localize('cancelTooltip', 'Cancel');
statusBarItem.command = cancelCommandId;
}
statusBarItem.show();
try {
await execution(tokenSource);
} catch (error) {
if (isCancelable && onCanceled && tokenSource.token.isCancellationRequested) {
// The error can be assumed to be due to cancelation occurring. Do the callback
onCanceled();
} else {
throw error;
}
} finally {
disposables.forEach(d => d.dispose());
}
}
private async confirmCancel(): Promise<boolean> {
return await this.prompter.promptSingle<boolean>(<IQuestion>{
type: QuestionTypes.confirm,
message: localize('cancel', 'Cancel operation?'),
default: true
});
}
}

View File

@@ -0,0 +1,92 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as loc from './localizedConstants';
// CONFIG VALUES ///////////////////////////////////////////////////////////
export const extensionConfigSectionName = 'dataManagement';
export const sqlConfigSectionName = 'sql';
export const configLogDebugInfo = 'logDebugInfo';
export const configProseParsingMaxLines = 'proseParsingMaxLines';
// SERVICE NAMES //////////////////////////////////////////////////////////
export const ObjectExplorerService = 'objectexplorer';
export const ViewType = 'view';
export enum BuiltInCommands {
SetContext = 'setContext'
}
export enum CommandContext {
WizardServiceEnabled = 'wizardservice:enabled'
}
export enum MssqlClusterItems {
Connection = 'mssqlCluster:connection',
Folder = 'mssqlCluster:folder',
File = 'mssqlCluster:file',
Error = 'mssqlCluster:error'
}
export enum HdfsItems {
Connection = 'hdfs:connection',
Folder = 'hdfs:folder',
File = 'hdfs:file',
Message = 'hdfs:message'
}
export enum HdfsItemsSubType {
Spark = 'hdfs:spark'
}
export enum AuthenticationType {
IntegratedAuthentication = 'Integrated',
UsernamePasswordAuthentication = 'Username Password',
SqlAuthentication = 'SqlLogin'
}
export const serviceCrashLink = 'https://github.com/Microsoft/vscode-mssql/wiki/SqlToolsService-Known-Issues';
export const serviceName = 'Data Virtualization Service';
export const providerId = 'dataManagement';
export const sqlFileExtension = 'sql';
export const virtualizeDataCommand = 'virtualizedatawizard.cmd.open';
export const virtualizeDataTask = 'virtualizedatawizard.task.open';
export const mssqlHdfsTableFromFileCommand = 'mssqlHdfsTableWizard.cmd.open';
export const ctp24Version = 'CTP2.4';
export const ctp25Version = 'CTP2.5';
export const ctp3Version = 'CTP3.0';
export const sql2019MajorVersion = 15;
export const delimitedTextFileType = 'DELIMITEDTEXT';
export enum DataSourceType {
SqlServer = 'SQL Server',
Oracle = 'Oracle',
SqlHDFS = 'SqlHDFS',
MongoDb = 'MongoDB',
Teradata = 'Teradata'
}
export const dataSourcePrefixMapping: Map<string, string> = new Map([
[DataSourceType.SqlServer, 'sqlserver://'],
[DataSourceType.Oracle, 'oracle://'],
[DataSourceType.MongoDb, 'mongodb://'],
[DataSourceType.Teradata, 'teradata://']
]);
export type ConnectionPageInfo = {
serverNameTitle: string,
databaseNameTitle: string,
isDbRequired: boolean
};
export const connectionPageInfoMapping: Map<string, ConnectionPageInfo> = new Map([
[DataSourceType.SqlServer, { serverNameTitle: loc.serverNameTitle, databaseNameTitle: loc.databaseNameTitle, isDbRequired: false }],
[DataSourceType.Oracle, { serverNameTitle: loc.hostnameTitle, databaseNameTitle: loc.serviceNameTitle, isDbRequired: true }],
[DataSourceType.MongoDb, { serverNameTitle: loc.serverNameTitle, databaseNameTitle: loc.databaseNameTitle, isDbRequired: false }],
[DataSourceType.Teradata, { serverNameTitle: loc.serverNameTitle, databaseNameTitle: loc.databaseNameTitle, isDbRequired: false }]
]);
export const proseMaxLinesDefault = 10000;

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.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import * as azdata from 'azdata';
import * as constants from './constants';
import * as utils from './utils';
import { ApiWrapper } from './apiWrapper';
import { AppContext } from './appContext';
import { DataSourceWizardService } from './services/contracts';
import { managerInstance, ApiType } from './services/serviceApiManager';
import { OpenVirtualizeDataWizardCommand, OpenVirtualizeDataWizardTask, OpenMssqlHdfsTableFromFileWizardCommand } from './wizards/wizardCommands';
import { ServiceClient } from './services/serviceClient';
export function activate(extensionContext: vscode.ExtensionContext): void {
let apiWrapper = new ApiWrapper();
let appContext = new AppContext(extensionContext, apiWrapper);
let wizard = managerInstance.onRegisteredApi<DataSourceWizardService>(ApiType.DataSourceWizard);
wizard((wizardService: DataSourceWizardService) => {
apiWrapper.setCommandContext(constants.CommandContext.WizardServiceEnabled, true);
extensionContext.subscriptions.push(new OpenVirtualizeDataWizardCommand(appContext, wizardService));
apiWrapper.registerTaskHandler(constants.virtualizeDataTask, (profile: azdata.IConnectionProfile) => {
new OpenVirtualizeDataWizardTask(appContext, wizardService).execute(profile);
});
extensionContext.subscriptions.push(new OpenMssqlHdfsTableFromFileWizardCommand(appContext, wizardService));
});
const outputChannel = apiWrapper.createOutputChannel(constants.serviceName);
let serviceClient = new ServiceClient(apiWrapper, outputChannel);
serviceClient.startService(extensionContext).then(success => undefined, err => {
apiWrapper.showErrorMessage(utils.getErrorMessage(err));
});
}

View File

@@ -0,0 +1,23 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as fs from 'fs';
export interface IFile {
path: string;
isDirectory: boolean;
}
export interface IFileSource {
enumerateFiles(path: string): Promise<IFile[]>;
mkdir(dirName: string, remoteBasePath: string): Promise<void>;
createReadStream(path: string): fs.ReadStream;
readFile(path: string, maxBytes?: number): Promise<Buffer>;
readFileLines(path: string, maxLines: number): Promise<Buffer>;
writeFile(localFile: IFile, remoteDir: string): Promise<string>;
delete(path: string, recursive?: boolean): Promise<void>;
exists(path: string): Promise<boolean>;
}

View File

@@ -0,0 +1,28 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import { ICommandViewContext, ICommandObjectExplorerContext } from './command';
import * as constants from './constants';
import * as LocalizedConstants from './localizedConstants';
import { AppContext } from './appContext';
import { TreeNode } from './treeNodes';
import { MssqlExtensionApi } from './typings/mssqlapis';
export async function getNodeFromMssqlProvider<T extends TreeNode>(context: ICommandViewContext | ICommandObjectExplorerContext, appContext: AppContext): Promise<T> {
let node: T = undefined;
if (context && context.type === constants.ViewType && context.node) {
node = context.node as T;
} else if (context && context.type === constants.ObjectExplorerService) {
let extensionApi: MssqlExtensionApi = vscode.extensions.getExtension('Microsoft.mssql').exports;
let mssqlObjectExplorerBrowser = extensionApi.getMssqlObjectExplorerBrowser();
node = <T><any>await mssqlObjectExplorerBrowser.getNode(context.explorerContext);
} else {
throw new Error(LocalizedConstants.msgMissingNodeContext);
}
return node;
}

View File

@@ -0,0 +1,133 @@
/*---------------------------------------------------------------------------------------------
* 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 fspath from 'path';
import * as fs from 'fs';
import * as Constants from './constants';
import { IFileSource } from './fileSources';
import { CancelableStream } from './cancelableStream';
import { TreeNode } from './treeNodes';
import { IFileNode } from './types';
export interface ITreeChangeHandler {
notifyNodeChanged(node: TreeNode): void;
}
export class TreeDataContext {
constructor(public extensionContext: vscode.ExtensionContext, public changeHandler: ITreeChangeHandler) { }
}
export abstract class HdfsFileSourceNode extends TreeNode {
constructor(protected context: TreeDataContext, protected _path: string, protected fileSource: IFileSource) {
super();
}
public get hdfsPath(): string {
return this._path;
}
public get nodePathValue(): string {
return this.getDisplayName();
}
getDisplayName(): string {
return fspath.basename(this._path);
}
public async delete(recursive: boolean = false): Promise<void> {
await this.fileSource.delete(this.hdfsPath, recursive);
// Notify parent should be updated. If at top, will return undefined which will refresh whole tree
(<HdfsFileSourceNode>this.parent).onChildRemoved();
this.context.changeHandler.notifyNodeChanged(this.parent);
}
public abstract onChildRemoved(): void;
}
export class FileNode extends HdfsFileSourceNode implements IFileNode {
constructor(context: TreeDataContext, path: string, fileSource: IFileSource) {
super(context, path, fileSource);
}
public onChildRemoved(): void {
// do nothing
}
getChildren(refreshChildren: boolean): TreeNode[] | Promise<TreeNode[]> {
return [];
}
getTreeItem(): vscode.TreeItem | Promise<vscode.TreeItem> {
let item = new vscode.TreeItem(this.getDisplayName(), vscode.TreeItemCollapsibleState.None);
item.iconPath = {
dark: this.context.extensionContext.asAbsolutePath('resources/dark/file_inverse.svg'),
light: this.context.extensionContext.asAbsolutePath('resources/light/file.svg')
};
item.contextValue = Constants.HdfsItems.File;
return item;
}
getNodeInfo(): azdata.NodeInfo {
// TODO improve node type handling so it's not tied to SQL Server types
let nodeInfo: azdata.NodeInfo = {
label: this.getDisplayName(),
isLeaf: true,
errorMessage: undefined,
metadata: undefined,
nodePath: this.generateNodePath(),
nodeStatus: undefined,
nodeType: Constants.HdfsItems.File,
nodeSubType: this.getSubType(),
iconType: 'FileGroupFile'
};
return nodeInfo;
}
public async getFileContentsAsString(maxBytes?: number): Promise<string> {
let contents: Buffer = await this.fileSource.readFile(this.hdfsPath, maxBytes);
return contents ? contents.toString('utf8') : '';
}
public async getFileLinesAsString(maxLines: number): Promise<string> {
let contents: Buffer = await this.fileSource.readFileLines(this.hdfsPath, maxLines);
return contents ? contents.toString('utf8') : '';
}
public writeFileContentsToDisk(localPath: string, cancelToken?: vscode.CancellationTokenSource): Promise<vscode.Uri> {
return new Promise((resolve, reject) => {
let readStream: fs.ReadStream = this.fileSource.createReadStream(this.hdfsPath);
let writeStream = fs.createWriteStream(localPath, {
encoding: 'utf8'
});
let cancelable = new CancelableStream(cancelToken);
cancelable.on('error', (err) => {
reject(err);
});
readStream.pipe(cancelable).pipe(writeStream);
let error: string | Error = undefined;
writeStream.on('error', (err) => {
error = err;
reject(error);
});
writeStream.on('finish', (location) => {
if (!error) {
resolve(vscode.Uri.file(localPath));
}
});
});
}
private getSubType(): string {
if (this.getDisplayName().toLowerCase().endsWith('.jar') || this.getDisplayName().toLowerCase().endsWith('.py')) {
return Constants.HdfsItemsSubType.Spark;
}
return undefined;
}
}

View File

@@ -0,0 +1,22 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vscode-nls';
const localize = nls.loadMessageBundle();
// General Constants ///////////////////////////////////////////////////////
export const SqlServerName = localize('sqlServerTypeName', 'SQL Server');
export const msgMissingNodeContext = localize('msgMissingNodeContext', 'Node Command called without any node passed');
// External Table
export const sourceSchemaTitle = localize('externalTable.sourceSchemaTitle', "Source Schema");
export const sourceTableTitle = localize('externalTable.sourceTableTitle', "Source Table");
export const externalSchemaTitle = localize('externalTable.externalSchemaTitle', "External Schema");
export const externalTableTitle = localize('externalTable.externalTableTitle', "External Table");
export const serverNameTitle = localize('externalTable.serverNameTitle', "Server Name");
export const hostnameTitle = localize('externalTable.hostnameTitle', "Hostname");
export const databaseNameTitle = localize('externalTable.databaseNameTitle', "Database Name");
export const serviceNameTitle = localize('externalTable.serviceNameTitle', "Service name / SID");

View File

@@ -0,0 +1,70 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
export class QuestionTypes {
public static get input(): string { return 'input'; }
public static get password(): string { return 'password'; }
public static get list(): string { return 'list'; }
public static get confirm(): string { return 'confirm'; }
public static get checkbox(): string { return 'checkbox'; }
public static get expand(): string { return 'expand'; }
}
// Question interface to clarify how to use the prompt feature
// based on Bower Question format: https://github.com/bower/bower/blob/89069784bb46bfd6639b4a75e98a0d7399a8c2cb/packages/bower-logger/README.md
export interface IQuestion {
// Type of question (see QuestionTypes)
type: string;
// Name of the question for disambiguation
name: string;
// Message to display to the user
message: string;
// Optional placeHolder to give more detailed information to the user
placeHolder?: any;
// Optional default value - this will be used instead of placeHolder
default?: any;
// optional set of choices to be used. Can be QuickPickItems or a simple name-value pair
choices?: Array<vscode.QuickPickItem | INameValueChoice>;
// Optional validation function that returns an error string if validation fails
validate?: (value: any) => string;
// Optional pre-prompt function. Takes in set of answers so far, and returns true if prompt should occur
shouldPrompt?: (answers: { [id: string]: any }) => boolean;
// Optional action to take on the question being answered
onAnswered?: (value: any) => void;
// Optional set of options to support matching choices.
matchOptions?: vscode.QuickPickOptions;
}
// Pair used to display simple choices to the user
export interface INameValueChoice {
name: string;
value: any;
}
// Generic object that can be used to define a set of questions and handle the result
export interface IQuestionHandler {
// Set of questions to be answered
questions: IQuestion[];
// Optional callback, since questions may handle themselves
callback?: IPromptCallback;
}
export interface IPrompter {
promptSingle<T>(question: IQuestion, ignoreFocusOut?: boolean): Promise<T>;
/**
* Prompts for multiple questions
*
* @returns {[questionId: string]: T} Map of question IDs to results, or undefined if
* the user canceled the question session
*/
prompt<T>(questions: IQuestion[], ignoreFocusOut?: boolean): Promise<{ [questionId: string]: any }>;
promptCallback(questions: IQuestion[], callback: IPromptCallback): void;
}
export interface IPromptCallback {
(answers: { [id: string]: any }): void;
}

View File

@@ -0,0 +1,332 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ClientCapabilities as VSClientCapabilities, RequestType, NotificationType } from 'vscode-languageclient';
import * as types from 'dataprotocol-client/lib/types';
import * as azdata from 'azdata';
/**
* @interface IMessage
*/
export interface IMessage {
jsonrpc: string;
}
// ------------------------------- < Telemetry Sent Event > ------------------------------------
/**
* Event sent when the language service send a telemetry event
*/
export namespace TelemetryNotification {
export const type = new NotificationType<TelemetryParams, void>('telemetry/sqlevent');
}
/**
* Update event parameters
*/
export class TelemetryParams {
public params: {
eventName: string;
properties: ITelemetryEventProperties;
measures: ITelemetryEventMeasures;
};
}
export interface ITelemetryEventProperties {
[key: string]: string;
}
export interface ITelemetryEventMeasures {
[key: string]: number;
}
// ------------------------------- </ Telemetry Sent Event > ----------------------------------
/*
* DataSourceWizardCreateSessionRequest
*/
export namespace DataSourceWizardCreateSessionRequest {
export const type = new RequestType<azdata.connection.ConnectionProfile, DataSourceWizardConfigInfoResponse, void, void>('datasourcewizard/createsession');
}
export interface DataSourceWizardConfigInfoResponse {
sessionId: string;
supportedSourceTypes: DataSourceType[];
databaseList: DatabaseOverview[];
serverMajorVersion: number;
productLevel: string;
}
export interface DatabaseOverview {
name: string;
hasMasterKey: boolean;
}
// Defines the important information about a type of data source - its name, configuration properties, etc.
export interface DataSourceType {
typeName: string;
authenticationTypes: string[];
}
/*
* DisposeWizardSessionRequest
*/
export namespace DisposeWizardSessionRequest {
export const type = new RequestType<string, boolean, void, void>('datasourcewizard/disposewizardsession');
}
/*
* ValidateVirtualizeDataInputRequest
*/
export namespace ValidateVirtualizeDataInputRequest {
export const type = new RequestType<VirtualizeDataInput, ValidateVirtualizeDataInputResponse, void, void>('datasourcewizard/validatevirtualizedatainput');
}
export interface ValidateVirtualizeDataInputResponse {
isValid: boolean;
errorMessages: string[];
}
export interface VirtualizeDataInput {
sessionId: string;
destDatabaseName: string;
sourceServerType: string;
destDbMasterKeyPwd: string;
existingDataSourceName: string;
newDataSourceName: string;
sourceServerName: string;
sourceDatabaseName: string;
sourceAuthenticationType: string;
existingCredentialName: string;
newCredentialName: string;
sourceUsername: string;
sourcePassword: string;
externalTableInfoList: ExternalTableInfo[];
newSchemas: string[];
}
export interface FileFormat {
formatName: string;
formatType: string;
fieldTerminator: string; // string token that separates columns on each line of the file
stringDelimiter: string; // string token that marks beginning/end of strings in the file
firstRow: number;
}
export interface ExternalTableInfo {
externalTableName: string[];
columnDefinitionList: ColumnDefinition[];
sourceTableLocation: string[];
fileFormat?: FileFormat;
}
export interface ColumnDefinition {
columnName: string;
dataType: string;
collationName: string;
isNullable: boolean;
isSupported?: boolean;
}
// TODO: All response objects for data-source-browsing request have this format, and can be formed with this generic class.
// Replace response objects with this class.
export interface ExecutionResult<T> {
isSuccess: boolean;
returnValue: T;
errorMessages: string[];
}
// TODO: All parameter objects for querying list of database, list of tables, and list of column definitions have this format,
// and can be formed with this generic class. Replace parameter objects with this class for those query requests.
export interface DataSourceBrowsingParams<T> {
virtualizeDataInput: VirtualizeDataInput;
querySubject: T;
}
export namespace GetSourceViewListRequest {
export const type = new RequestType<DataSourceBrowsingParams<string>, ExecutionResult<SchemaViews[]>, void, void>('datasourcewizard/getsourceviewlist');
}
/*
* GetDatabaseInfoRequest
*/
export namespace GetDatabaseInfoRequest {
export const type = new RequestType<GetDatabaseInfoRequestParams, GetDatabaseInfoResponse, void, void>('datasourcewizard/getdatabaseinfo');
}
export interface GetDatabaseInfoResponse {
isSuccess: boolean;
errorMessages: string[];
databaseInfo: DatabaseInfo;
}
export interface DatabaseInfo {
hasMasterKey: boolean;
defaultSchema: string;
schemaList: string[];
existingCredentials: CredentialInfo[];
externalDataSources: DataSourceInstance[];
externalTables: TableInfo[];
externalFileFormats: string[];
}
export interface CredentialInfo {
credentialName: string;
username: string;
}
export interface TableInfo {
schemaName: string;
tableName: string;
}
export interface GetDatabaseInfoRequestParams {
sessionId: string;
databaseName: string;
}
// Defines the important information about an external data source that has already been created.
export interface DataSourceInstance {
name: string;
location: string;
authenticationType: string;
username?: string;
credentialName?: string;
}
/*
* ProcessVirtualizeDataInputRequest
*/
export namespace ProcessVirtualizeDataInputRequest {
export const type = new RequestType<VirtualizeDataInput, ProcessVirtualizeDataInputResponse, void, void>('datasourcewizard/processvirtualizedatainput');
}
export interface ProcessVirtualizeDataInputResponse {
isSuccess: boolean;
errorMessages: string[];
}
export namespace GenerateScriptRequest {
export const type = new RequestType<VirtualizeDataInput, GenerateScriptResponse, void, void>('datasourcewizard/generatescript');
}
export interface GenerateScriptResponse {
isSuccess: boolean;
errorMessages: string[];
script: string;
}
/*
* GetSourceDatabasesRequest
*/
export namespace GetSourceDatabasesRequest {
export const type = new RequestType<VirtualizeDataInput, GetSourceDatabasesResponse, void, void>('datasourcewizard/getsourcedatabaselist');
}
export interface GetSourceDatabasesResponse {
isSuccess: boolean;
errorMessages: string[];
databaseNames: string[];
}
/*
* GetSourceTablesRequest
*/
export namespace GetSourceTablesRequest {
export const type = new RequestType<GetSourceTablesRequestParams, GetSourceTablesResponse, void, void>('datasourcewizard/getsourcetablelist');
}
export interface GetSourceTablesRequestParams {
sessionId: string;
virtualizeDataInput: VirtualizeDataInput;
sourceDatabaseName: string;
}
export interface GetSourceTablesResponse {
isSuccess: boolean;
errorMessages: string[];
schemaTablesList: SchemaTables[];
}
export interface SchemaTables {
schemaName: string;
tableNames: string[];
}
export interface SchemaViews {
schemaName: string;
viewNames: string[];
}
/*
* GetSourceColumnDefinitionsRequest
*/
export namespace GetSourceColumnDefinitionsRequest {
export const type = new RequestType<GetSourceColumnDefinitionsRequestParams, GetSourceColumnDefinitionsResponse, void, void>('datasourcewizard/getsourcecolumndefinitionlist');
}
export interface GetSourceColumnDefinitionsRequestParams {
sessionId: string;
virtualizeDataInput: VirtualizeDataInput;
location: string[];
}
export interface GetSourceColumnDefinitionsResponse {
isSuccess: boolean;
errorMessages: string[];
columnDefinitions: ColumnDefinition[];
}
/*
* Prose
*/
export interface ColumnInfo {
name: string;
sqlType: string;
isNullable: boolean;
}
export interface ProseDiscoveryParams {
filePath: string;
tableName: string;
schemaName?: string;
fileType?: string;
fileContents?: string;
}
export interface ProseDiscoveryResponse {
dataPreview: string[][];
columnInfo: ColumnInfo[];
columnDelimiter: string;
firstRow: number;
quoteCharacter: string;
}
export namespace ProseDiscoveryRequest {
export const type = new RequestType<ProseDiscoveryParams, ProseDiscoveryResponse, void, void>('flatfile/proseDiscovery');
}
// ------------------------------- < Data Source Wizard API definition > ------------------------------------
export interface DataSourceWizardService {
providerId?: string;
createDataSourceWizardSession(requestParams: azdata.connection.ConnectionProfile): Thenable<DataSourceWizardConfigInfoResponse>;
disposeWizardSession(sessionId: string): Thenable<boolean>;
validateVirtualizeDataInput(requestParams: VirtualizeDataInput): Thenable<ValidateVirtualizeDataInputResponse>;
getDatabaseInfo(requestParams: GetDatabaseInfoRequestParams): Thenable<GetDatabaseInfoResponse>;
processVirtualizeDataInput(requestParams: VirtualizeDataInput): Thenable<ProcessVirtualizeDataInputResponse>;
generateScript(requestParams: VirtualizeDataInput): Thenable<GenerateScriptResponse>;
getSourceDatabases(requestParams: VirtualizeDataInput): Thenable<GetSourceDatabasesResponse>;
getSourceTables(requestParams: GetSourceTablesRequestParams): Thenable<GetSourceTablesResponse>;
getSourceViewList(requestParams: DataSourceBrowsingParams<string>): Thenable<ExecutionResult<SchemaViews[]>>;
getSourceColumnDefinitions(requestParams: GetSourceColumnDefinitionsRequestParams): Thenable<GetSourceColumnDefinitionsResponse>;
sendProseDiscoveryRequest(requestParams: ProseDiscoveryParams): Thenable<ProseDiscoveryResponse>;
}

View File

@@ -0,0 +1,189 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { SqlOpsDataClient, SqlOpsFeature } from 'dataprotocol-client';
import { ClientCapabilities, StaticFeature, RPCMessageType, ServerCapabilities } from 'vscode-languageclient';
import * as UUID from 'vscode-languageclient/lib/utils/uuid';
import { Disposable } from 'vscode';
import * as azdata from 'azdata';
import { Telemetry } from './telemetry';
import * as serviceUtils from './serviceUtils';
import {
TelemetryNotification,
DataSourceWizardCreateSessionRequest, DataSourceWizardConfigInfoResponse,
DataSourceWizardService, DisposeWizardSessionRequest, VirtualizeDataInput,
ValidateVirtualizeDataInputResponse, GetDatabaseInfoRequestParams, GetDatabaseInfoResponse,
ProcessVirtualizeDataInputResponse, GenerateScriptResponse, ValidateVirtualizeDataInputRequest,
GetDatabaseInfoRequest, ProcessVirtualizeDataInputRequest, GenerateScriptRequest, GetSourceDatabasesResponse,
GetSourceDatabasesRequest, GetSourceTablesResponse, GetSourceTablesRequestParams, GetSourceTablesRequest,
GetSourceColumnDefinitionsRequestParams, GetSourceColumnDefinitionsResponse, GetSourceColumnDefinitionsRequest,
ProseDiscoveryParams, ProseDiscoveryResponse, ProseDiscoveryRequest, DataSourceBrowsingParams, ExecutionResult, GetSourceViewListRequest, SchemaViews
} from './contracts';
import { managerInstance, ApiType } from './serviceApiManager';
export class TelemetryFeature implements StaticFeature {
constructor(private _client: SqlOpsDataClient) { }
fillClientCapabilities(capabilities: ClientCapabilities): void {
serviceUtils.ensure(capabilities, 'telemetry')!.telemetry = true;
}
initialize(): void {
this._client.onNotification(TelemetryNotification.type, e => {
Telemetry.sendTelemetryEvent(e.params.eventName, e.params.properties, e.params.measures);
});
}
}
export class DataSourceWizardFeature extends SqlOpsFeature<undefined> {
private static readonly messagesTypes: RPCMessageType[] = [
DataSourceWizardCreateSessionRequest.type
];
constructor(client: SqlOpsDataClient) {
super(client, DataSourceWizardFeature.messagesTypes);
}
public fillClientCapabilities(capabilities: ClientCapabilities): void {
// ensure(ensure(capabilities, 'connection')!, 'objectExplorer')!.dynamicRegistration = true;
}
public initialize(capabilities: ServerCapabilities): void {
this.register(this.messages, {
id: UUID.generateUuid(),
registerOptions: undefined
});
}
protected registerProvider(options: undefined): Disposable {
const client = this._client;
let createDataSourceWizardSession = (requestParams: azdata.connection.ConnectionProfile): Thenable<DataSourceWizardConfigInfoResponse> => {
return client.sendRequest(DataSourceWizardCreateSessionRequest.type, requestParams).then(
r => r,
e => {
client.logFailedRequest(DataSourceWizardCreateSessionRequest.type, e);
return Promise.reject(e);
}
);
};
let disposeWizardSession = (sessionId: string): Thenable<boolean> => {
return client.sendRequest(DisposeWizardSessionRequest.type, sessionId).then(
r => r,
e => {
client.logFailedRequest(DisposeWizardSessionRequest.type, e);
return Promise.reject(e);
}
);
};
let validateVirtualizeDataInput = (requestParams: VirtualizeDataInput): Thenable<ValidateVirtualizeDataInputResponse> => {
return client.sendRequest(ValidateVirtualizeDataInputRequest.type, requestParams).then(
r => r,
e => {
client.logFailedRequest(ValidateVirtualizeDataInputRequest.type, e);
return Promise.reject(e);
}
);
};
let getDatabaseInfo = (requestParams: GetDatabaseInfoRequestParams): Thenable<GetDatabaseInfoResponse> => {
return client.sendRequest(GetDatabaseInfoRequest.type, requestParams).then(
r => r,
e => {
client.logFailedRequest(GetDatabaseInfoRequest.type, e);
return Promise.reject(e);
}
);
};
let processVirtualizeDataInput = (requestParams: VirtualizeDataInput): Thenable<ProcessVirtualizeDataInputResponse> => {
return client.sendRequest(ProcessVirtualizeDataInputRequest.type, requestParams).then(
r => r,
e => {
client.logFailedRequest(ProcessVirtualizeDataInputRequest.type, e);
return Promise.reject(e);
}
);
};
let generateScript = (requestParams: VirtualizeDataInput): Thenable<GenerateScriptResponse> => {
return client.sendRequest(GenerateScriptRequest.type, requestParams).then(
r => r,
e => {
client.logFailedRequest(GenerateScriptRequest.type, e);
return Promise.reject(e);
}
);
};
let getSourceDatabases = (requestParams: VirtualizeDataInput): Thenable<GetSourceDatabasesResponse> => {
return client.sendRequest(GetSourceDatabasesRequest.type, requestParams).then(
r => r,
e => {
client.logFailedRequest(GetSourceDatabasesRequest.type, e);
return Promise.reject(e);
}
);
};
let getSourceTables = (requestParams: GetSourceTablesRequestParams): Thenable<GetSourceTablesResponse> => {
return client.sendRequest(GetSourceTablesRequest.type, requestParams).then(
r => r,
e => {
client.logFailedRequest(GetSourceTablesRequest.type, e);
return Promise.reject(e);
}
);
};
let getSourceViewList = (requestParams: DataSourceBrowsingParams<string>): Thenable<ExecutionResult<SchemaViews[]>> => {
return client.sendRequest(GetSourceViewListRequest.type, requestParams).then(
r => r,
e => {
client.logFailedRequest(GetSourceViewListRequest.type, e);
return Promise.reject(e);
}
);
};
let getSourceColumnDefinitions = (requestParams: GetSourceColumnDefinitionsRequestParams): Thenable<GetSourceColumnDefinitionsResponse> => {
return client.sendRequest(GetSourceColumnDefinitionsRequest.type, requestParams).then(
r => r,
e => {
client.logFailedRequest(GetSourceColumnDefinitionsRequest.type, e);
return Promise.reject(e);
}
);
};
let sendProseDiscoveryRequest = (requestParams: ProseDiscoveryParams): Thenable<ProseDiscoveryResponse> => {
return client.sendRequest(ProseDiscoveryRequest.type, requestParams).then(
r => r,
e => {
client.logFailedRequest(ProseDiscoveryRequest.type, e);
return Promise.reject(e);
}
);
};
return managerInstance.registerApi<DataSourceWizardService>(ApiType.DataSourceWizard, {
providerId: client.providerId,
createDataSourceWizardSession,
disposeWizardSession,
validateVirtualizeDataInput,
getDatabaseInfo,
processVirtualizeDataInput,
generateScript,
getSourceDatabases,
getSourceTables,
getSourceViewList,
getSourceColumnDefinitions,
sendProseDiscoveryRequest
});
}
}

View File

@@ -0,0 +1,75 @@
/*---------------------------------------------------------------------------------------------
* 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 '../apiWrapper';
export enum ApiType {
ExplorerProvider = 'ExplorerProvider',
DataSourceWizard = 'DataSourceWizard'
}
export interface IServiceApi {
onRegisteredApi<T>(type: ApiType): vscode.Event<T>;
registerApi<T>(type: ApiType, feature: T): vscode.Disposable;
}
export interface IModelViewDefinition {
id: string;
modelView: azdata.ModelView;
}
class ServiceApiManager implements IServiceApi {
private modelViewRegistrations: { [id: string]: boolean } = {};
private featureEventChannels: { [type: string]: vscode.EventEmitter<any> } = {};
private _onRegisteredModelView = new vscode.EventEmitter<IModelViewDefinition>();
public onRegisteredApi<T>(type: ApiType): vscode.Event<T> {
let featureEmitter = this.featureEventChannels[type];
if (!featureEmitter) {
featureEmitter = new vscode.EventEmitter<T>();
this.featureEventChannels[type] = featureEmitter;
}
return featureEmitter.event;
}
public registerApi<T>(type: ApiType, feature: T): vscode.Disposable {
let featureEmitter = this.featureEventChannels[type];
if (featureEmitter) {
featureEmitter.fire(feature);
}
// TODO handle unregistering API on close
return {
dispose: () => undefined
};
}
public get onRegisteredModelView(): vscode.Event<IModelViewDefinition> {
return this._onRegisteredModelView.event;
}
public registerModelView(id: string, modelView: azdata.ModelView): void {
this._onRegisteredModelView.fire({
id: id,
modelView: modelView
});
}
/**
* Performs a one-time registration of a model view provider, where this will be
* hooked to an event handler instead of having a predefined method that uses the model view
*/
public ensureModelViewRegistered(id: string, apiWrapper: ApiWrapper): any {
if (!this.modelViewRegistrations[id]) {
apiWrapper.registerModelViewProvider(id, (modelView) => {
this.registerModelView(id, modelView);
});
this.modelViewRegistrations[id] = true;
}
}
}
export let managerInstance = new ServiceApiManager();

View File

@@ -0,0 +1,171 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { SqlOpsDataClient, ClientOptions } from 'dataprotocol-client';
import { IConfig, ServerProvider, Events, LogLevel } from '@microsoft/ads-service-downloader';
import { ServerOptions, TransportKind } from 'vscode-languageclient';
import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
const localize = nls.loadMessageBundle();
import * as path from 'path';
import { EventAndListener } from 'eventemitter2';
import { Telemetry, LanguageClientErrorHandler } from './telemetry';
import { ApiWrapper } from '../apiWrapper';
import * as Constants from '../constants';
import { TelemetryFeature, DataSourceWizardFeature } from './features';
import { promises as fs } from 'fs';
export class ServiceClient {
private statusView: vscode.StatusBarItem;
constructor(private apiWrapper: ApiWrapper, private outputChannel: vscode.OutputChannel) {
this.statusView = this.apiWrapper.createStatusBarItem(vscode.StatusBarAlignment.Left);
}
public async startService(context: vscode.ExtensionContext): Promise<void> {
const rawConfig = await fs.readFile(path.join(context.extensionPath, 'config.json'));
const config: IConfig = JSON.parse(rawConfig.toString());
config.installDirectory = path.join(context.extensionPath, config.installDirectory);
config.proxy = this.apiWrapper.getConfiguration('http').get('proxy');
config.strictSSL = this.apiWrapper.getConfiguration('http').get('proxyStrictSSL') || true;
const serverdownloader = new ServerProvider(config);
serverdownloader.eventEmitter.onAny(this.generateHandleServerProviderEvent());
let clientOptions: ClientOptions = this.createClientOptions();
const installationStart = Date.now();
let client: SqlOpsDataClient;
return new Promise((resolve, reject) => {
serverdownloader.getOrDownloadServer().then(e => {
const installationComplete = Date.now();
let serverOptions = this.generateServerOptions(e, context);
client = new SqlOpsDataClient(Constants.serviceName, serverOptions, clientOptions);
const processStart = Date.now();
client.onReady().then(() => {
const processEnd = Date.now();
this.statusView.text = localize('serviceStarted', 'Service Started');
setTimeout(() => {
this.statusView.hide();
}, 1500);
Telemetry.sendTelemetryEvent('startup/LanguageClientStarted', {
installationTime: String(installationComplete - installationStart),
processStartupTime: String(processEnd - processStart),
totalTime: String(processEnd - installationStart),
beginningTimestamp: String(installationStart)
});
});
this.statusView.show();
this.statusView.text = localize('serviceStarting', 'Starting service');
let disposable = client.start();
context.subscriptions.push(disposable);
resolve();
}, e => {
Telemetry.sendTelemetryEvent('ServiceInitializingFailed');
this.apiWrapper.showErrorMessage(localize('serviceStartFailed', 'Failed to start Scale Out Data service:{0}', e));
// Just resolve to avoid unhandled promise. We show the error to the user.
resolve();
});
});
}
private createClientOptions(): ClientOptions {
return {
providerId: Constants.providerId,
errorHandler: new LanguageClientErrorHandler(),
synchronize: {
configurationSection: [Constants.extensionConfigSectionName, Constants.sqlConfigSectionName]
},
features: [
// we only want to add new features
TelemetryFeature,
DataSourceWizardFeature
],
outputChannel: new CustomOutputChannel()
};
}
private generateServerOptions(executablePath: string, context: vscode.ExtensionContext): ServerOptions {
let launchArgs = [];
launchArgs.push('--log-dir');
let logFileLocation = context['logPath'];
launchArgs.push(logFileLocation);
let config = vscode.workspace.getConfiguration(Constants.extensionConfigSectionName);
if (config) {
let logDebugInfo = config[Constants.configLogDebugInfo];
if (logDebugInfo) {
launchArgs.push('--enable-logging');
}
}
return { command: executablePath, args: launchArgs, transport: TransportKind.stdio };
}
private generateHandleServerProviderEvent(): EventAndListener {
let dots = 0;
return (e: string, ...args: any[]) => {
switch (e) {
case Events.INSTALL_START:
this.outputChannel.show(true);
this.statusView.show();
this.outputChannel.appendLine(localize('installingServiceDetailed', "Installing {0} to {1}", Constants.serviceName, args[0]));
this.statusView.text = localize('installingService', "Installing {0}", Constants.serviceName);
break;
case Events.INSTALL_END:
this.outputChannel.appendLine(localize('serviceInstalled', "Installed {0}", Constants.serviceName));
break;
case Events.DOWNLOAD_START:
this.outputChannel.appendLine(localize('downloadingService', "Downloading {0}", args[0]));
this.outputChannel.append(localize('downloadingServiceSize', "({0} KB)", Math.ceil(args[1] / 1024).toLocaleString(vscode.env.language)));
this.statusView.text = localize('downloadingServiceStatus', "Downloading {0}", Constants.serviceName);
break;
case Events.DOWNLOAD_PROGRESS:
let newDots = Math.ceil(args[0] / 5);
if (newDots > dots) {
this.outputChannel.append('.'.repeat(newDots - dots));
dots = newDots;
}
break;
case Events.DOWNLOAD_END:
this.outputChannel.appendLine(localize('downloadingServiceComplete', "Done downloading {0}", Constants.serviceName));
break;
case Events.LOG_EMITTED:
if (args[0] >= LogLevel.Warning) {
this.outputChannel.appendLine(args[1]);
}
break;
default:
console.error(`Unknown event from Server Provider ${e}`);
break;
}
};
}
}
class CustomOutputChannel implements vscode.OutputChannel {
name: string;
append(value: string): void {
console.log(value);
}
appendLine(value: string): void {
console.log(value);
}
clear(): void {
}
show(preserveFocus?: boolean): void;
show(column?: vscode.ViewColumn, preserveFocus?: boolean): void;
show(column?: any, preserveFocus?: any): void {
}
hide(): void {
}
dispose(): void {
}
replace(value: string): void {
throw new Error('Method not implemented.');
}
}

View File

@@ -0,0 +1,62 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as path from 'path';
import * as crypto from 'crypto';
import * as os from 'os';
export function ensure(target: object, key: string): any {
if (target[key] === void 0) {
target[key] = {} as any;
}
return target[key];
}
export function generateUserId(): Promise<string> {
return new Promise<string>(resolve => {
try {
let interfaces = os.networkInterfaces();
let mac;
for (let key of Object.keys(interfaces)) {
let item = interfaces[key][0];
if (!item.internal) {
mac = item.mac;
break;
}
}
if (mac) {
resolve(crypto.createHash('sha256').update(mac + os.homedir(), 'utf8').digest('hex'));
} else {
resolve(generateGuid());
}
} catch (err) {
resolve(generateGuid()); // fallback
}
});
}
export function generateGuid(): string {
let hexValues: string[] = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'];
// c.f. rfc4122 (UUID version 4 = xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx)
let oct: string = '';
let tmp: number;
/* tslint:disable:no-bitwise */
for (let a: number = 0; a < 4; a++) {
tmp = (4294967296 * Math.random()) | 0;
oct += hexValues[tmp & 0xF] +
hexValues[tmp >> 4 & 0xF] +
hexValues[tmp >> 8 & 0xF] +
hexValues[tmp >> 12 & 0xF] +
hexValues[tmp >> 16 & 0xF] +
hexValues[tmp >> 20 & 0xF] +
hexValues[tmp >> 24 & 0xF] +
hexValues[tmp >> 28 & 0xF];
}
// 'Set the two most significant bits (bits 6 and 7) of the clock_seq_hi_and_reserved to zero and one, respectively'
let clockSequenceHi: string = hexValues[8 + (Math.random() * 4) | 0];
return oct.substr(0, 8) + '-' + oct.substr(9, 4) + '-4' + oct.substr(13, 3) + '-' + clockSequenceHi + oct.substr(16, 3) + '-' + oct.substr(19, 12);
/* tslint:enable:no-bitwise */
}

View File

@@ -0,0 +1,216 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ErrorAction, CloseAction } from 'vscode-languageclient';
import TelemetryReporter from '@microsoft/ads-extension-telemetry';
import { PlatformInformation } from '@microsoft/ads-service-downloader/out/platform';
import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
const localize = nls.loadMessageBundle();
import { ApiWrapper } from '../apiWrapper';
import * as constants from '../constants';
import * as serviceUtils from './serviceUtils';
import { IMessage, ITelemetryEventProperties, ITelemetryEventMeasures } from './contracts';
/**
* Handle Language Service client errors
* @class LanguageClientErrorHandler
*/
export class LanguageClientErrorHandler {
/**
* Creates an instance of LanguageClientErrorHandler.
* @memberOf LanguageClientErrorHandler
*/
constructor(private apiWrapper?: ApiWrapper) {
if (!this.apiWrapper) {
this.apiWrapper = new ApiWrapper();
}
}
/**
* Show an error message prompt with a link to known issues wiki page
* @memberOf LanguageClientErrorHandler
*/
showOnErrorPrompt(): void {
// TODO add telemetry
// Telemetry.sendTelemetryEvent('SqlToolsServiceCrash');
let crashButtonText = localize('serviceCrashButton', 'View Known Issues');
this.apiWrapper.showErrorMessage(
localize('serviceCrashMessage', 'service component could not start'),
crashButtonText
).then(action => {
if (action && action === crashButtonText) {
vscode.env.openExternal(vscode.Uri.parse(constants.serviceCrashLink));
}
});
}
/**
* Callback for language service client error
*
* @param error
* @param message
* @param count
* @returns
*
* @memberOf LanguageClientErrorHandler
*/
error(error: Error, message: IMessage, count: number): ErrorAction {
this.showOnErrorPrompt();
// we don't retry running the service since crashes leave the extension
// in a bad, unrecovered state
return ErrorAction.Shutdown;
}
/**
* Callback for language service client closed
*
* @returns
*
* @memberOf LanguageClientErrorHandler
*/
closed(): CloseAction {
this.showOnErrorPrompt();
// we don't retry running the service since crashes leave the extension
// in a bad, unrecovered state
return CloseAction.DoNotRestart;
}
}
/**
* Filters error paths to only include source files. Exported to support testing
*/
export function FilterErrorPath(line: string): string {
if (line) {
let values: string[] = line.split('/out/');
if (values.length <= 1) {
// Didn't match expected format
return line;
} else {
return values[1];
}
}
}
export class Telemetry {
private static reporter: TelemetryReporter;
private static userId: string;
private static platformInformation: PlatformInformation;
private static disabled: boolean;
// Get the unique ID for the current user of the extension
public static getUserId(): Promise<string> {
return new Promise<string>(resolve => {
// Generate the user id if it has not been created already
if (typeof this.userId === 'undefined') {
let id = serviceUtils.generateUserId();
id.then(newId => {
this.userId = newId;
resolve(this.userId);
});
} else {
resolve(this.userId);
}
});
}
public static getPlatformInformation(): Promise<PlatformInformation> {
if (this.platformInformation) {
return Promise.resolve(this.platformInformation);
} else {
return new Promise<PlatformInformation>(resolve => {
PlatformInformation.getCurrent().then(info => {
this.platformInformation = info;
resolve(this.platformInformation);
});
});
}
}
/**
* Disable telemetry reporting
*/
public static disable(): void {
this.disabled = true;
}
/**
* Initialize the telemetry reporter for use.
*/
public static initialize(): void {
if (typeof this.reporter === 'undefined') {
// Check if the user has opted out of telemetry
if (!vscode.workspace.getConfiguration('telemetry').get<boolean>('enableTelemetry', true)) {
this.disable();
return;
}
let packageInfo = vscode.extensions.getExtension('Microsoft.datavirtualization').packageJSON;
this.reporter = new TelemetryReporter(packageInfo.name, packageInfo.version, packageInfo.aiKey);
}
}
/**
* Send a telemetry event for an exception
*/
public static sendTelemetryEventForException(
err: any, methodName: string, extensionConfigName: string): void {
try {
let stackArray: string[];
let firstLine: string = '';
if (err !== undefined && err.stack !== undefined) {
stackArray = err.stack.split('\n');
if (stackArray !== undefined && stackArray.length >= 2) {
firstLine = stackArray[1]; // The fist line is the error message and we don't want to send that telemetry event
firstLine = FilterErrorPath(firstLine);
}
}
// Only adding the method name and the fist line of the stack trace. We don't add the error message because it might have PII
this.sendTelemetryEvent('Exception', { methodName: methodName, errorLine: firstLine });
// Utils.logDebug('Unhandled Exception occurred. error: ' + err + ' method: ' + methodName, extensionConfigName);
} catch (telemetryErr) {
// If sending telemetry event fails ignore it so it won't break the extension
// Utils.logDebug('Failed to send telemetry event. error: ' + telemetryErr, extensionConfigName);
}
}
/**
* Send a telemetry event using application insights
*/
public static sendTelemetryEvent(
eventName: string,
properties?: ITelemetryEventProperties,
measures?: ITelemetryEventMeasures): void {
if (typeof this.disabled === 'undefined') {
this.disabled = false;
}
if (this.disabled || typeof (this.reporter) === 'undefined') {
// Don't do anything if telemetry is disabled
return;
}
if (!properties || typeof properties === 'undefined') {
properties = {};
}
// Augment the properties structure with additional common properties before sending
Promise.all([this.getUserId(), this.getPlatformInformation()]).then(() => {
properties['userId'] = this.userId;
properties['distribution'] = (this.platformInformation && this.platformInformation.distribution) ?
`${this.platformInformation.distribution.name}, ${this.platformInformation.distribution.version}` : '';
this.reporter.sendTelemetryEvent(eventName, properties, measures);
});
}
}
Telemetry.initialize();

View File

@@ -0,0 +1,28 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as IstanbulTestRunner from '@microsoft/vscodetestcover';
let testRunner: any = IstanbulTestRunner;
// You can directly control Mocha options by uncommenting the following lines
// See https://github.com/mochajs/mocha/wiki/Using-mocha-programmatically#set-options for more info
testRunner.configure(
// Mocha Options
{
ui: 'bdd', // the TDD UI is being used in extension.test.ts (suite, test, etc.)
reporter: 'pm-mocha-jenkins-reporter',
reporterOptions: {
junit_report_name: 'Extension Tests',
junit_report_path: __dirname + '/../../test-reports/extension_tests.xml',
junit_report_stack: 1
},
useColors: true // colored output from test results
},
// Coverage configuration options
{
coverConfig: '../../coverconfig.json'
});
module.exports = testRunner;

View File

@@ -0,0 +1,47 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as fs from 'fs';
import { IFileSource, IFile } from '../fileSources';
export class MockFileSource implements IFileSource {
filesToReturn: Map<string, IFile[]>;
constructor() {
this.filesToReturn = new Map<string, IFile[]>();
}
enumerateFiles(filePath: string): Promise<IFile[]> {
let files: IFile[] = this.filesToReturn.get(filePath);
return Promise.resolve(files);
}
mkdir(dirName: string, remoteBasePath: string): Promise<void> {
return Promise.resolve(undefined);
}
writeFile(localFile: IFile, remoteDir: string): Promise<string> {
return Promise.resolve(undefined);
}
delete(filePath: string): Promise<void> {
throw new Error('Method not implemented.');
}
readFile(filePath: string, maxBytes?: number): Promise<Buffer> {
throw new Error('Method not implemented.');
}
readFileLines(path: string, maxLines: number): Promise<Buffer> {
throw new Error("Method not implemented.");
}
createReadStream(filePath: string): fs.ReadStream {
throw new Error('Method not implemented.');
}
exists(filePath: string): Promise<boolean> {
throw new Error('Method not implemented.');
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,563 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as TypeMoq from 'typemoq';
import * as should from 'should';
import * as path from 'path';
import * as azdata from 'azdata';
import { VirtualizeDataMockEnv, MockDataSourceService, MockInputBoxComponent, MockDropdownComponent, MockTextComponent, MockLoadingComponent, MockDeclarativeTableComponent, MockTableComponent, MockConnectionProfile, MockButtonComponent } from './stubs';
import { TableFromFileWizard } from '../wizards/tableFromFile/tableFromFileWizard';
import { ImportDataModel } from '../wizards/tableFromFile/api/models';
import { FileConfigPage, FileConfigPageUiElements } from '../wizards/tableFromFile/pages/fileConfigPage';
import { ProsePreviewPage, ProsePreviewPageUiElements } from '../wizards/tableFromFile/pages/prosePreviewPage';
import { ModifyColumnsPage, ModifyColumnsPageUiElements } from '../wizards/tableFromFile/pages/modifyColumnsPage';
import { SummaryPage, SummaryPageUiElements } from '../wizards/tableFromFile/pages/summaryPage';
import { FileNode } from '../hdfsProvider';
import { DataSourceInstance } from '../services/contracts';
import { stripUrlPathSlashes } from '../utils';
import { DataSourceType, delimitedTextFileType } from '../constants';
describe('Table From File Wizard:', function () {
let env = new VirtualizeDataMockEnv();
let appContext = env.getMockedAppContext();
let service = new MockDataSourceService();
let mockWizard = TypeMoq.Mock.ofType(TableFromFileWizard, undefined, undefined, appContext, service);
describe('File Config Page Tests', function () {
let mockPage = env.getMockedWizardPage();
let model = <ImportDataModel>{};
let mockService = TypeMoq.Mock.ofType(MockDataSourceService);
let page = new FileConfigPage(mockWizard.object, mockPage, model, undefined, mockService.object);
let ui: FileConfigPageUiElements = {
fileTextBox: new MockTextComponent(),
serverTextBox: new MockTextComponent(),
databaseDropdown: new MockDropdownComponent(),
dataSourceDropdown: new MockDropdownComponent(),
tableNameTextBox: new MockInputBoxComponent(),
schemaDropdown: new MockDropdownComponent(),
databaseLoader: new MockLoadingComponent(),
dataSourceLoader: new MockLoadingComponent(),
schemaLoader: new MockLoadingComponent(),
fileFormatNameTextBox: new MockInputBoxComponent(),
refreshButton: new MockButtonComponent()
};
page.setUi(ui);
model.allDatabases = ['TestDb'];
model.serverConn = new MockConnectionProfile();
mockService.setup(s => s.createDataSourceWizardSession(TypeMoq.It.isAny())).returns(() => service.createDataSourceWizardSession(undefined));
mockService.setup(s => s.getDatabaseInfo(TypeMoq.It.isAny())).returns(() => service.getDatabaseInfo(undefined));
let onPageEnterTest = async function (tableName: string) {
(<any>page).pageSetupComplete = false;
await page.onPageEnter();
should(ui.fileTextBox.value).be.equal(model.parentFile.filePath);
should(ui.serverTextBox.value).be.equal(model.serverConn.serverName);
should(ui.databaseDropdown.value).be.equal('TestDb');
should(ui.dataSourceDropdown.value).be.equal('TestSource');
should(ui.tableNameTextBox.value).be.equal(tableName);
should(ui.schemaDropdown.value).be.equal('TestSchema');
should(ui.fileFormatNameTextBox.value).be.equal(`FileFormat_${tableName}`);
should(ui.databaseLoader.loading).be.false();
should(ui.databaseDropdown.enabled).be.true();
should(ui.dataSourceLoader.loading).be.false();
should(ui.schemaLoader.loading).be.false();
should(ui.refreshButton.enabled).be.true();
should(model.sessionId).be.equal('TestSessionId');
should(model.allDatabases.length).be.equal(1);
should(model.allDatabases[0]).be.equal('TestDb');
should(model.fileType).be.equal('TXT');
should(model.database).be.equal('TestDb');
should(model.existingDataSource).be.equal('TestSource');
should(model.table).be.equal(tableName);
should(model.existingSchema).be.equal('TestSchema');
should(model.newSchema).be.undefined();
should(model.fileFormat).be.equal(`FileFormat_${tableName}`);
};
let mockFileNode = TypeMoq.Mock.ofType(FileNode);
model.proseParsingFile = mockFileNode.object;
it('OnPageEnter Test', async function () {
// With file
let tableName = 'TestFile';
model.parentFile = {
isFolder: false,
filePath: path.join('BaseDir', 'AnotherDir', `${tableName}.csv`)
};
mockFileNode.setup(node => node.hdfsPath).returns(() => model.parentFile.filePath);
await onPageEnterTest(tableName);
// With existing session
model.sessionId = 'OldTestId';
model.allDatabases = ['OldTestDb1', 'OldTestDb2'];
mockService.setup(s => s.disposeWizardSession(TypeMoq.It.isAny())).returns(() => Promise.resolve(true));
await onPageEnterTest(tableName);
mockService.verify(s => s.disposeWizardSession(TypeMoq.It.isAny()), TypeMoq.Times.once());
// With folder
tableName = 'CsvTest';
model.parentFile = {
isFolder: true,
filePath: path.join('BaseDir', 'AnotherDir', tableName)
};
mockFileNode.setup(node => node.hdfsPath).returns(() => path.join(model.parentFile.filePath, 'TestFile.csv'));
await onPageEnterTest(tableName);
});
it('OnPageLeave Test', async function () {
model.existingDataSource = undefined;
(await page.onPageLeave(true)).should.be.false();
model.existingDataSource = '';
(await page.onPageLeave(true)).should.be.false();
model.existingDataSource = 'TestSource';
model.existingSchema = undefined;
(await page.onPageLeave(true)).should.be.false();
model.existingSchema = '';
(await page.onPageLeave(true)).should.be.false();
model.existingSchema = 'TestSchema';
(await page.onPageLeave(true)).should.be.true();
model.existingSchema = undefined;
model.newSchema = 'NewTestSchema';
(await page.onPageLeave(true)).should.be.true();
model.fileFormat = 'TestExternalFileFormat';
(await page.onPageLeave(true)).should.be.false();
model.fileFormat = 'NotAnExistingFileFormat';
(await page.onPageLeave(true)).should.be.true();
// Existing table, but using new schema
model.table = 'TestExternalTable';
(await page.onPageLeave(true)).should.be.true();
model.existingSchema = 'TestSchema';
model.newSchema = undefined;
model.table = 'TestExternalTable';
(await page.onPageLeave(true)).should.be.false();
model.table = 'NotAnExistingFileTable';
(await page.onPageLeave(true)).should.be.true();
ui.databaseLoader.loading = true;
(await page.onPageLeave(false)).should.be.false();
ui.databaseLoader.loading = false;
ui.dataSourceLoader.loading = true;
(await page.onPageLeave(false)).should.be.false();
ui.dataSourceLoader.loading = false;
ui.schemaLoader.loading = true;
(await page.onPageLeave(false)).should.be.false();
ui.schemaLoader.loading = false;
(await page.onPageLeave(false)).should.be.true();
});
it('Data Sources Test', async function () {
let dbInfo = await service.getDatabaseInfo(undefined);
let sessionInfo = await service.createDataSourceWizardSession(undefined);
model.parentFile = {
isFolder: false,
filePath: path.join('BaseDir', 'AnotherDir', 'TestFile.csv')
};
let setupMocks = () => {
mockService.setup(s => s.getDatabaseInfo(TypeMoq.It.isAny())).returns(() => Promise.resolve(dbInfo));
mockService.setup(s => s.createDataSourceWizardSession(TypeMoq.It.isAny())).returns(() => Promise.resolve(sessionInfo));
mockFileNode.setup(node => node.hdfsPath).returns(() => model.parentFile.filePath);
};
let testDataSource = (productVersion: string, dataSourceName: string, dataSourceLocation: string) => {
should(model.versionInfo.productLevel).be.equal(productVersion);
should(model.existingDataSource).be.undefined();
should(model.newDataSource).not.be.undefined();
should(model.newDataSource.name).be.equal(dataSourceName);
should(model.newDataSource.location).be.equal(dataSourceLocation);
should(model.newDataSource.authenticationType).be.undefined();
should(model.newDataSource.credentialName).be.undefined();
should(model.newDataSource.username).be.undefined();
};
let testNewCtp24Source = (dataSourceName: string = 'SqlStoragePool') => {
testDataSource('CTP2.4', dataSourceName, 'sqlhdfs://service-master-pool:50070/');
};
let testNewCtp25Source = (dataSourceName: string = 'SqlStoragePool') => {
testDataSource('CTP2.5', dataSourceName, 'sqlhdfs://nmnode-0-svc:50070/');
};
let testNewCtp3Source = (dataSourceName: string = 'SqlStoragePool') => {
testDataSource('CTP3.0', dataSourceName, 'sqlhdfs://controller-svc:8080/default');
};
setupMocks();
await (<any>page).refreshPage();
should(model.versionInfo.productLevel).be.equal('CTP3.1');
should(model.existingDataSource).be.equal('TestSource');
should(model.newDataSource).be.undefined();
sessionInfo.productLevel = 'CTP2.4';
setupMocks();
await (<any>page).refreshPage();
testNewCtp24Source();
sessionInfo.productLevel = 'CTP2.5';
setupMocks();
await (<any>page).refreshPage();
testNewCtp25Source();
dbInfo.databaseInfo.externalDataSources = [];
sessionInfo.productLevel = 'CTP3.0';
setupMocks();
await (<any>page).refreshPage();
testNewCtp3Source();
dbInfo.databaseInfo.externalDataSources = [<DataSourceInstance>{
name: 'RandomSource',
location: 'sqlhdfs://NotARealSource:50070/'
}, <DataSourceInstance>{
name: 'SqlStoragePool',
location: 'sqlhdfs://NotARealSource:50070/'
}, <DataSourceInstance>{
name: 'SqlStoragePool1',
location: 'sqlhdfs://NotARealSource1:8080/default'
}, <DataSourceInstance>{
name: 'SqlStoragePool2',
location: 'sqlhdfs://NotARealSource2:8080/default'
}];
setupMocks();
await (<any>page).refreshPage();
testNewCtp3Source('SqlStoragePool3');
sessionInfo.productLevel = 'CTP2.4';
setupMocks();
await (<any>page).refreshPage();
testNewCtp24Source('SqlStoragePool3');
sessionInfo.serverMajorVersion = 1000;
sessionInfo.productLevel = 'NotARealCtpVersion';
setupMocks();
await (<any>page).refreshPage();
// Default to the latest version's data source location
testDataSource(sessionInfo.productLevel, 'SqlStoragePool3', 'sqlhdfs://controller-svc/default');
});
it('Refresh Test', async function () {
let dbInfo = await service.getDatabaseInfo(undefined);
let sessionInfo = await service.createDataSourceWizardSession(undefined);
model.parentFile = {
isFolder: false,
filePath: path.join('BaseDir', 'AnotherDir', 'TestFile.csv')
};
let setupMocks = () => {
mockService.setup(s => s.getDatabaseInfo(TypeMoq.It.isAny())).returns(() => Promise.resolve(dbInfo));
mockService.setup(s => s.createDataSourceWizardSession(TypeMoq.It.isAny())).returns(() => Promise.resolve(sessionInfo));
mockFileNode.setup(node => node.hdfsPath).returns(() => model.parentFile.filePath);
};
setupMocks();
let testStr = 'RefreshTestStr';
await (<any>page).refreshPage();
should(model.database).not.be.equal(testStr);
should(model.existingDataSource).not.be.equal(testStr);
should(model.table).not.be.equal(testStr);
should(model.existingSchema).not.be.equal(testStr);
should(model.fileFormat).not.be.equal(`FileFormat_${testStr}`);
sessionInfo.databaseList = [{ name: testStr, hasMasterKey: false }];
dbInfo.databaseInfo.externalDataSources[0].name = testStr;
model.parentFile = {
isFolder: false,
filePath: path.join('BaseDir', 'AnotherDir', `${testStr}.csv`)
};
dbInfo.databaseInfo.schemaList = [testStr];
setupMocks();
await (<any>page).refreshPage();
should(model.database).be.equal(testStr);
should(model.existingDataSource).be.equal(testStr);
should(model.table).be.equal(testStr);
should(model.existingSchema).be.equal(testStr);
should(model.fileFormat).be.equal(`FileFormat_${testStr}`);
});
});
describe('Preview Page Tests', function () {
let mockPage = env.getMockedWizardPage();
let fileNodeMock = TypeMoq.Mock.ofType(FileNode);
let model = <ImportDataModel>{};
model.proseParsingFile = fileNodeMock.object;
fileNodeMock.setup(f => f.getFileLinesAsString(TypeMoq.It.isAny())).returns(() => Promise.resolve(service.proseTestData));
let page = new ProsePreviewPage(mockWizard.object, mockPage, model, undefined, service);
let ui: ProsePreviewPageUiElements = {
table: new MockTableComponent(),
loading: new MockLoadingComponent()
};
page.setUi(ui);
it('OnPageEnter Test', async function () {
await page.onPageEnter();
should(ui.loading.loading).be.false();
should(model.columnDelimiter).be.equal(',');
should(model.firstRow).be.equal(2);
should(model.quoteCharacter).be.equal('"');
should(ui.table.columns.length).be.equal(2);
should(ui.table.columns[0]).be.equal('TestId');
should(ui.table.columns[1]).be.equal('TestStr');
should(ui.table.data.length).be.equal(1);
should(ui.table.data[0].length).be.equal(2);
should(ui.table.data[0][0]).be.equal('1');
should(ui.table.data[0][1]).be.equal('abc');
});
it('OnPageEnter Error Test', async function () {
let errorMockWizard = TypeMoq.Mock.ofType(TableFromFileWizard, undefined, undefined, appContext, service);
let errorFileNodeMock = TypeMoq.Mock.ofType(FileNode);
let errorModel = <ImportDataModel>{};
errorModel.proseParsingFile = errorFileNodeMock.object;
let errorMsg = 'Expected Test Error';
errorFileNodeMock.setup(f => f.getFileLinesAsString(TypeMoq.It.isAny())).throws(new Error(errorMsg));
errorMockWizard.setup(w => w.showErrorMessage(TypeMoq.It.isValue(errorMsg)));
let errorPage = new ProsePreviewPage(errorMockWizard.object, mockPage, errorModel, undefined, service);
errorMockWizard.verify((w => w.showErrorMessage(TypeMoq.It.isValue(errorMsg))), TypeMoq.Times.never());
errorPage.setUi({
table: new MockTableComponent(),
loading: new MockLoadingComponent()
});
await errorPage.onPageEnter();
errorMockWizard.verify((w => w.showErrorMessage(TypeMoq.It.isValue(errorMsg))), TypeMoq.Times.once());
});
it('OnPageLeave Test', async function () {
(await page.onPageLeave(true)).should.be.true();
});
});
describe('Modify Columns Page Tests', function () {
let mockPage = env.getMockedWizardPage();
let model = <ImportDataModel>{};
let page = new ModifyColumnsPage(mockWizard.object, mockPage, model, undefined, service);
let ui: ModifyColumnsPageUiElements = {
table: new MockDeclarativeTableComponent,
loading: new MockLoadingComponent,
text: new MockTextComponent()
};
page.setUi(ui);
model.proseColumns = [{
columnName: 'TestId',
dataType: 'int',
collationName: undefined,
isNullable: false
}, {
columnName: 'TestStr',
dataType: 'varchar(50)',
collationName: undefined,
isNullable: true
}];
it('OnPageEnter Test', async function () {
await page.onPageEnter();
should(ui.loading.loading).be.false();
should(ui.table.data.length).be.equal(2);
should(ui.table.data[0][0]).be.equal('TestId');
should(ui.table.data[0][1]).be.equal('int');
should(ui.table.data[0][2]).be.equal(false);
should(ui.table.data[1][0]).be.equal('TestStr');
should(ui.table.data[1][1]).be.equal('varchar(50)');
should(ui.table.data[1][2]).be.equal(true);
});
it('OnPageLeave Test', async function () {
(await page.onPageLeave(true)).should.be.true();
});
});
describe('Summary Page Tests', function () {
let mockPage = env.getMockedWizardPage();
let model = <ImportDataModel>{};
let page = new SummaryPage(mockWizard.object, mockPage, model, undefined, service);
let ui: SummaryPageUiElements = {
table: new MockTableComponent()
};
page.setUi(ui);
it('OnPageEnter Test', async function () {
model.serverConn = <azdata.connection.ConnectionProfile>{
providerId: undefined,
connectionId: undefined,
serverName: 'TestServer'
};
model.database = 'TestDb';
model.table = 'TestTable';
model.existingSchema = 'dbo';
model.fileFormat = 'TestFileFormat';
model.parentFile = {
isFolder: false,
filePath: path.join('BaseDir', 'AnotherDir', 'TestTable.csv')
};
mockWizard.setup(w => w.changeDoneButtonLabel(TypeMoq.It.isValue('Virtualize Data')));
mockWizard.setup(w => w.setGenerateScriptVisibility(TypeMoq.It.isValue(true)));
await page.onPageEnter();
mockWizard.verify(w => w.changeDoneButtonLabel(TypeMoq.It.isValue('Virtualize Data')), TypeMoq.Times.once());
mockWizard.verify(w => w.setGenerateScriptVisibility(TypeMoq.It.isValue(true)), TypeMoq.Times.once());
should(ui.table.data[0][1]).be.equal(model.serverConn.serverName);
should(ui.table.data[1][1]).be.equal(model.database);
should(ui.table.data[2][1]).be.equal(model.table);
should(ui.table.data[3][1]).be.equal(model.existingSchema);
should(ui.table.data[4][1]).be.equal(model.fileFormat);
should(ui.table.data[5][1]).be.equal(model.parentFile.filePath);
});
it('OnPageLeave Test', async function () {
mockWizard.setup(w => w.changeDoneButtonLabel(TypeMoq.It.isValue('Next')));
mockWizard.setup(w => w.setGenerateScriptVisibility(TypeMoq.It.isValue(false)));
(await page.onPageLeave(true)).should.be.true();
mockWizard.verify(w => w.changeDoneButtonLabel(TypeMoq.It.isValue('Next')), TypeMoq.Times.once());
mockWizard.verify(w => w.setGenerateScriptVisibility(TypeMoq.It.isValue(false)), TypeMoq.Times.once());
});
});
describe('Utilities Tests', function () {
it('Generate Input From Model Test', function () {
let input = TableFromFileWizard.generateInputFromModel(undefined);
should(input).be.undefined();
let model = <ImportDataModel>{
sessionId: 'TestId',
columnDelimiter: ',',
database: 'TestDatabase',
fileFormat: 'FileFormat_TestTable',
firstRow: 1,
newDataSource: {
name: 'SqlStoragePool',
location: 'sqlhdfs://controller-svc:8080/default/',
authenticationType: undefined,
username: undefined,
credentialName: undefined
},
newSchema: 'TestSchema',
parentFile: {
filePath: 'test/TestTable.csv',
isFolder: false
},
proseColumns: [{
collationName: undefined,
columnName: 'column1',
dataType: 'nvarchar(50)',
isNullable: true,
}],
quoteCharacter: '\"',
table: 'TestTable'
};
input = TableFromFileWizard.generateInputFromModel(model);
should(input).not.be.undefined();
should(input.sessionId).be.equal(model.sessionId);
should(input.destDatabaseName).be.equal(model.database);
should(input.sourceServerType).be.equal(DataSourceType.SqlHDFS);
should(input.externalTableInfoList).not.be.undefined();
should(input.externalTableInfoList.length).be.equal(1);
let tableInfo = input.externalTableInfoList[0];
should(tableInfo.externalTableName).not.be.undefined();
should(tableInfo.externalTableName.length).be.equal(2);
should(tableInfo.externalTableName[0]).be.equal(model.newSchema);
should(tableInfo.externalTableName[1]).be.equal(model.table);
should(tableInfo.columnDefinitionList).not.be.undefined();
should(tableInfo.columnDefinitionList.length).be.equal(1);
let columnInfo = tableInfo.columnDefinitionList[0];
let proseInfo = model.proseColumns[0];
should(columnInfo.collationName).be.equal(proseInfo.collationName);
should(columnInfo.columnName).be.equal(proseInfo.columnName);
should(columnInfo.dataType).be.equal(proseInfo.dataType);
should(columnInfo.isNullable).be.equal(proseInfo.isNullable);
should(tableInfo.sourceTableLocation).not.be.undefined();
should(tableInfo.sourceTableLocation.length).be.equal(1);
should(tableInfo.sourceTableLocation[0]).be.equal(model.parentFile.filePath);
should(tableInfo.fileFormat).not.be.undefined();
should(tableInfo.fileFormat.formatName).be.equal(model.fileFormat);
should(tableInfo.fileFormat.formatType).be.equal(delimitedTextFileType);
should(tableInfo.fileFormat.fieldTerminator).be.equal(model.columnDelimiter);
should(tableInfo.fileFormat.stringDelimiter).be.equal(model.quoteCharacter);
should(tableInfo.fileFormat.firstRow).be.equal(model.firstRow);
should(input.newDataSourceName).be.equal(model.newDataSource.name);
should(input.sourceServerName).be.equal('controller-svc:8080/default/');
should(input.existingDataSourceName).be.undefined();
should(input.newSchemas).not.be.undefined();
should(input.newSchemas.length).be.equal(1);
should(input.newSchemas[0]).be.equal(model.newSchema);
});
it('Remove URL Path Slashes Test', function () {
let testPath = undefined;
should(stripUrlPathSlashes(testPath)).be.equal('');
testPath = '';
should(stripUrlPathSlashes(testPath)).be.equal('');
testPath = '//////////';
should(stripUrlPathSlashes(testPath)).be.equal('');
testPath = 'a/';
should(stripUrlPathSlashes(testPath)).be.equal('a');
testPath = 'testPath';
should(stripUrlPathSlashes(testPath)).be.equal('testPath');
testPath = '/testPath';
should(stripUrlPathSlashes(testPath)).be.equal('testPath');
testPath = '/testPath/testPath2';
should(stripUrlPathSlashes(testPath)).be.equal('testPath/testPath2');
testPath = 'testPath/testPath2';
should(stripUrlPathSlashes(testPath)).be.equal('testPath/testPath2');
testPath = 'testPath/testPath2///';
should(stripUrlPathSlashes(testPath)).be.equal('testPath/testPath2');
testPath = '/testPath/testPath2///';
should(stripUrlPathSlashes(testPath)).be.equal('testPath/testPath2');
testPath = '/testPath/testPath2/testPath3/////';
should(stripUrlPathSlashes(testPath)).be.equal('testPath/testPath2/testPath3');
});
});
});

View File

@@ -0,0 +1,19 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
export async function assertThrowsAsync(fn: Function, msg: string): Promise<void> {
let f = () => {
// Empty
};
try {
await fn();
} catch (e) {
f = () => { throw e; };
} finally {
assert.throws(f, undefined, msg);
}
}

View File

@@ -0,0 +1,375 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as TypeMoq from 'typemoq';
import * as azdata from 'azdata';
import * as should from 'should';
import { VirtualizeDataModel } from '../wizards/virtualizeData/virtualizeDataModel';
import { CheckboxTreeNode } from '../wizards/virtualizeData/virtualizeDataTree';
import {
MockInputBoxComponent, MockWizard, VirtualizeDataMockEnv, MockTextComponent,
MockDeclarativeTableComponent, MockDataSourceService, MockConnectionProfile
} from './stubs';
import { CreateMasterKeyPage, MasterKeyUiElements } from '../wizards/virtualizeData/createMasterKeyPage';
import { SummaryPage, SummaryUiElements } from '../wizards/virtualizeData/summaryPage';
import { VDIManager } from '../wizards/virtualizeData/virtualizeDataInputManager';
import { ColumnDefinition } from '../services/contracts';
describe('Wizard Setup Tests', function (): void {
it('Should set model fields after creating session.', async () => {
let mockWizard = new MockWizard();
mockWizard.message = undefined;
let vdiManager = (new VirtualizeDataMockEnv()).getMockedVDIManager();
let model = new VirtualizeDataModel(new MockConnectionProfile(), new MockDataSourceService(), new MockWizard(), vdiManager);
await model.createSession();
should(model.sessionId).be.eql('TestSessionId');
should(model.destDatabaseList.length).be.greaterThan(0);
should(model.destDatabaseList).containEql({ name: 'TestDb', hasMasterKey: false });
});
});
describe('MasterKeyPage Tests', function (): void {
it('[MasterKeyPage Test] MasterKeyPage should create a wizard page and register content during the initialization.', async () => {
let env = new VirtualizeDataMockEnv();
let mockDataModel = env.getMockedVirtualizeDataModel();
let vdiManager = env.getMockedVDIManager();
let appContext = env.getMockedAppContext();
let apiWrapper = env.getApiWrapperMock();
let wizardPage = env.getWizardPageMock();
let masterKeyPage = new CreateMasterKeyPage(mockDataModel, vdiManager, appContext);
apiWrapper.verifyAll();
wizardPage.verifyAll();
should(masterKeyPage).not.be.null();
});
it('[MasterKeyPage Test] MasterKeyPage should get data from VirtualizeDataModel when the page is opened.', async () => {
let uiElement = new MasterKeyUiElements();
uiElement.masterKeyPasswordInput = new MockInputBoxComponent();
uiElement.masterKeyPasswordConfirmInput = new MockInputBoxComponent();
let env = new VirtualizeDataMockEnv();
env.getMockEnvConstants().destDbMasterKeyPwd = undefined;
let dataModel = env.getMockedVirtualizeDataModel();
let dataModelMock = env.getVirtualizeDataModelMock();
let vdiManager = env.getMockedVDIManager();
let appContext = env.getMockedAppContext();
let masterKeyPage = new CreateMasterKeyPage(dataModel, vdiManager, appContext);
masterKeyPage.setUi(uiElement);
await masterKeyPage.updatePage();
dataModelMock.verify(x => x.hasMasterKey(), TypeMoq.Times.once());
should(uiElement.masterKeyPasswordInput.enabled).be.false;
should(uiElement.masterKeyPasswordConfirmInput.enabled).be.false;
env.getMockEnvConstants().destDbMasterKeyPwd = 'Chanel$4700'; // [SuppressMessage("Microsoft.Security", "CS001:SecretInline", Justification="Unit test - not actually used to authenticate")]
await masterKeyPage.updatePage();
should(uiElement.masterKeyPasswordInput.enabled).be.true;
should(uiElement.masterKeyPasswordConfirmInput.enabled).be.true;
});
it('[MasterKeyPage Test] MasterKeyPage should fail validation if page content is invalid.', async () => {
let uiElement = new MasterKeyUiElements();
uiElement.masterKeyPasswordInput = new MockInputBoxComponent();
uiElement.masterKeyPasswordConfirmInput = new MockInputBoxComponent();
let env = new VirtualizeDataMockEnv();
let dataModel = env.getMockedVirtualizeDataModel();
let dataModelMock = env.getVirtualizeDataModelMock();
let vdiManager = env.getMockedVDIManager();
let vdiManagerMock = env.getVDIManagerMock();
let appContext = env.getMockedAppContext();
let masterKeyPage = new CreateMasterKeyPage(dataModel, vdiManager, appContext);
masterKeyPage.setUi(uiElement);
uiElement.masterKeyPasswordInput.value = 'test123';
uiElement.masterKeyPasswordConfirmInput.value = '123test';
should(await masterKeyPage.validate()).be.false;
dataModelMock.verify(x => x.showWizardError(TypeMoq.It.isAny()), TypeMoq.Times.once());
dataModelMock.verify(x => x.validateInput(TypeMoq.It.isAny()), TypeMoq.Times.never());
vdiManagerMock.verify(x => x.getVirtualizeDataInput(TypeMoq.It.isAny()), TypeMoq.Times.never());
});
it('[MasterKeyPage Test] MasterKeyPage should pass validation if page content is valid.', async () => {
let uiElement = new MasterKeyUiElements();
uiElement.masterKeyPasswordInput = new MockInputBoxComponent();
uiElement.masterKeyPasswordConfirmInput = new MockInputBoxComponent();
let env = new VirtualizeDataMockEnv();
let dataModel = env.getMockedVirtualizeDataModel();
let dataModelMock = env.getVirtualizeDataModelMock();
let vdiManager = env.getMockedVDIManager();
let vdiManagerMock = env.getVDIManagerMock();
let appContext = env.getMockedAppContext();
let masterKeyPage = new CreateMasterKeyPage(dataModel, vdiManager, appContext);
masterKeyPage.setUi(uiElement);
uiElement.masterKeyPasswordInput.value = 'test123';
uiElement.masterKeyPasswordConfirmInput.value = 'test123';
should(await masterKeyPage.validate()).be.true;
dataModelMock.verify(x => x.showWizardError(TypeMoq.It.isAny()), TypeMoq.Times.never());
dataModelMock.verify(x => x.validateInput(TypeMoq.It.isAny()), TypeMoq.Times.once());
vdiManagerMock.verify(x => x.getVirtualizeDataInput(TypeMoq.It.isAny()), TypeMoq.Times.once());
});
});
describe('ObjectMappingPage Tests', function (): void {
it('[ObjectMappingPage Test] ObjectMappingPage should create a wizard page and register content during the initialization.', async () => {
let env = new VirtualizeDataMockEnv();
let objectMappingPage = env.getMockedObjectMappingPage();
let apiWrapper = env.getApiWrapperMock();
let wizardPage = env.getWizardPageMock();
apiWrapper.verify(x => x.createWizardPage(TypeMoq.It.isAnyString()), TypeMoq.Times.once());
wizardPage.verify(x => x.registerContent(TypeMoq.It.isAny()), TypeMoq.Times.once());
should(objectMappingPage).not.be.undefined();
});
it('[ObjectMappingPage Test] ObjectMappingPage should get data from VirtualizeDataModel when the page is opened.', async () => {
let env = new VirtualizeDataMockEnv();
let objectMappingPage = env.getMockedObjectMappingPage();
await objectMappingPage.updatePage();
let omPage = <any>objectMappingPage;
let rootNode = omPage._treeRootNode;
should(rootNode).not.be.undefined();
let databaseNodes = await rootNode.getChildren();
should(databaseNodes).not.be.undefined();
should(databaseNodes.length > 0).be.true();
let dataModelMock = env.getVirtualizeDataModelMock();
dataModelMock.verify(x => x.getSourceDatabases(TypeMoq.It.isAny()), TypeMoq.Times.atLeastOnce());
});
it('[ObjectMappingPage Test] Database node should generate children, and mark them as checked when database node is checked before generating children.', async () => {
let env = new VirtualizeDataMockEnv();
let objectMappingPage = env.getMockedObjectMappingPage();
await objectMappingPage.updatePage();
let omPage = <any>objectMappingPage;
let rootNode: CheckboxTreeNode = omPage._treeRootNode as CheckboxTreeNode;
let databaseNodes: CheckboxTreeNode[] = await rootNode.getChildren();
should(databaseNodes).not.be.undefined();
should(databaseNodes.length > 0).be.true();
should(databaseNodes[0].hasChildren).be.false();
await omPage.actionOnNodeCheckStatusChanged(databaseNodes[0], true);
should(databaseNodes[0].hasChildren).be.true();
let children: CheckboxTreeNode[] = await databaseNodes[0].getChildren();
let tableFolderNode: CheckboxTreeNode = children[0];
let viewFolderNode: CheckboxTreeNode = children[1];
should(tableFolderNode.checked).be.true();
should(viewFolderNode.checked).be.true();
should(tableFolderNode.hasChildren).be.true();
should(viewFolderNode.hasChildren).be.true();
let tableNodes: CheckboxTreeNode[] = await tableFolderNode.getChildren();
tableNodes.forEach(tableNode => {
should(tableNode.checked).be.true();
});
let viewNodes: CheckboxTreeNode[] = await viewFolderNode.getChildren();
viewNodes.forEach(viewNode => {
should(viewNode.checked).be.true();
});
});
it('[ObjectMappingPage Test] Column definition mapping table should be updated when table or view node is selected.', async () => {
let env = new VirtualizeDataMockEnv();
let objectMappingPage = env.getMockedObjectMappingPage();
await objectMappingPage.updatePage();
let omPage = <any>objectMappingPage;
let rootNode: CheckboxTreeNode = omPage._treeRootNode as CheckboxTreeNode;
let databaseNodes: CheckboxTreeNode[] = await rootNode.getChildren();
should(databaseNodes).not.be.undefined();
should(databaseNodes.length > 0).be.true();
let folderNodes: CheckboxTreeNode[] = await databaseNodes[0].getChildren();
should(folderNodes).not.be.undefined();
should(folderNodes.length === 2).be.true();
let tableFolderNodes: CheckboxTreeNode = folderNodes[0];
let tableNodes: CheckboxTreeNode[] = await tableFolderNodes.getChildren();
should(tableNodes).not.be.undefined();
should(tableNodes.length > 0).be.true();
let viewFolderNodes: CheckboxTreeNode = folderNodes[1];
let viewNodes: CheckboxTreeNode[] = await viewFolderNodes.getChildren();
should(viewNodes).not.be.undefined();
should(viewNodes.length > 0).be.true();
let colDefTable = omPage._columnMappingTable as azdata.DeclarativeTableComponent;
colDefTable.updateProperties({ data: undefined });
should(colDefTable.data).be.undefined();
await omPage.actionOnNodeIsSelected(tableNodes[0]);
should(colDefTable.data).not.be.undefined();
should(colDefTable.data.length > 0).be.true();
colDefTable.data.forEach(row => {
should(row.length > 0).be.true();
should(row[0] !== undefined && row[0] !== '').be.true();
});
let colDefs = (await omPage._dataSourceBrowser.getColumnDefinitions((<any>tableNodes[0]).location)) as ColumnDefinition[];
for (let i = 0; i < colDefs.length; ++i) {
should(colDefs[i].columnName === colDefTable.data[i][0]).be.true();
should(colDefs[i].columnName === colDefTable.data[i][1]).be.true();
should(colDefs[i].dataType === colDefTable.data[i][2]).be.true();
should(colDefs[i].isNullable === colDefTable.data[i][3]).be.true();
should(colDefs[i].collationName === colDefTable.data[i][4]).be.true();
}
colDefTable.updateProperties({ data: undefined });
should(colDefTable.data).be.undefined();
await omPage.actionOnNodeIsSelected(viewNodes[0]);
should(colDefTable.data).not.be.undefined();
should(colDefTable.data.length > 0).be.true();
colDefTable.data.forEach(row => {
should(row.length > 0).be.true();
should(row[0] !== undefined && row[0] !== '').be.true();
});
colDefs = (await omPage._dataSourceBrowser.getColumnDefinitions((<any>viewNodes[0]).location)) as ColumnDefinition[];
for (let i = 0; i < colDefs.length; ++i) {
should(colDefs[i].columnName === colDefTable.data[i][0]).be.true();
should(colDefs[i].columnName === colDefTable.data[i][1]).be.true();
should(colDefs[i].dataType === colDefTable.data[i][2]).be.true();
should(colDefs[i].isNullable === colDefTable.data[i][3]).be.true();
should(colDefs[i].collationName === colDefTable.data[i][4]).be.true();
}
});
it('[ObjectMappingPage Test] ObjectMappingPage should return table information for creation for checked tables', async () => {
let env = new VirtualizeDataMockEnv();
let objectMappingPage = env.getMockedObjectMappingPage();
await objectMappingPage.updatePage();
let omPage = <any>objectMappingPage;
let rootNode: CheckboxTreeNode = omPage._treeRootNode as CheckboxTreeNode;
let databaseNodes: CheckboxTreeNode[] = await rootNode.getChildren();
for (let i = 0; i < databaseNodes.length; ++i) {
await omPage.actionOnNodeCheckStatusChanged(databaseNodes[i], true);
}
let inputValues = VDIManager.getEmptyInputInstance();
omPage.getInputValues(inputValues);
should(inputValues).not.be.undefined();
should(inputValues.externalTableInfoList).not.be.undefined();
let tablesToBeCreated = inputValues.externalTableInfoList;
should(tablesToBeCreated.length > 0).be.true();
});
it('[ObjectMappingPage Test] Database nodes and table nodes should be able to expand successfully.', async () => {
let env = new VirtualizeDataMockEnv();
let objectMappingPage = env.getMockedObjectMappingPage();
await objectMappingPage.updatePage();
let omPage = <any>objectMappingPage;
let rootNode: CheckboxTreeNode = omPage._treeRootNode as CheckboxTreeNode;
let databaseNodes: CheckboxTreeNode[] = await rootNode.getChildren();
should(rootNode.hasChildren).be.true();
should(databaseNodes).not.be.undefined();
should(databaseNodes.length > 0).be.true();
for (let i = 0; i < databaseNodes.length; ++i) {
let tableNodes: CheckboxTreeNode[] = await databaseNodes[i].getChildren();
should(databaseNodes[i].hasChildren).be.True();
should(tableNodes).not.be.undefined();
should(tableNodes.length > 0).be.true();
}
});
it('[ObjectMappingPage Test] Tree view should be updated every time objectMappingPage is loaded', async () => {
let env = new VirtualizeDataMockEnv();
let objectMappingPage = env.getMockedObjectMappingPage();
await objectMappingPage.updatePage();
let omPage = <any>objectMappingPage;
let rootNode: CheckboxTreeNode = omPage._treeRootNode as CheckboxTreeNode;
should(rootNode.hasChildren).be.false();
let databaseNodes: CheckboxTreeNode[] = await rootNode.getChildren();
should(rootNode.hasChildren).be.true();
await objectMappingPage.updatePage();
rootNode = omPage._treeRootNode as CheckboxTreeNode;
should(rootNode.hasChildren).be.false();
});
it('[ObjectMappingPage Test] ObjectMappingPage should show table help text when the page is updated', async () => {
let env = new VirtualizeDataMockEnv();
let objectMappingPage = env.getMockedObjectMappingPage();
await objectMappingPage.updatePage();
let omPage = <any>objectMappingPage;
let objectMappingWrapper = omPage._objectMappingWrapper;
should(objectMappingWrapper).not.be.undefined();
should(objectMappingWrapper.items.length === 1).be.true();
});
});
describe('Summary Page Tests', function (): void {
it('[SummaryPage Test] SummaryPage should create a wizard page and register content during the initialization.', async () => {
let env = new VirtualizeDataMockEnv();
let dataModel = env.getMockedVirtualizeDataModel();
let vdiManager = env.getMockedVDIManager();
let apiContext = env.getMockedAppContext();
let apiWrapperMock = env.getApiWrapperMock();
let wizardPageMock = env.getWizardPageMock();
let summaryPage = new SummaryPage(dataModel, vdiManager, apiContext);
wizardPageMock.verify(x => x.registerContent(TypeMoq.It.isAny()), TypeMoq.Times.once());
apiWrapperMock.verify(x => x.createWizardPage(TypeMoq.It.isAnyString()), TypeMoq.Times.once());
should(summaryPage).not.be.null();
});
it('[SummaryPage Test] SummaryPage should get data from VirtualizeDataModel when the page is opened.', async () => {
let uiElement = new SummaryUiElements();
uiElement.destDBLabel = new MockTextComponent();
uiElement.summaryTable = new MockDeclarativeTableComponent();
let env = new VirtualizeDataMockEnv();
let dataModel = env.getMockedVirtualizeDataModel();
let vdiManager = env.getMockedVDIManager();
let apiContext = env.getMockedAppContext();
let vdiManagerMock = env.getVDIManagerMock();
let summaryPage = new SummaryPage(dataModel, vdiManager, apiContext);
summaryPage.setUi(uiElement);
await summaryPage.updatePage();
vdiManagerMock.verify(x => x.getVirtualizeDataInput(TypeMoq.It.isAny()), TypeMoq.Times.once());
let testData = env.getMockEnvConstants();
should(uiElement.destDBLabel.value).be.eql(testData.destDbNameSelected);
let summaryHasKey = function (key: string): boolean {
return uiElement.summaryTable.data.some(row => row && row[0] === key);
};
let summaryHasValue = function (value: string): boolean {
return uiElement.summaryTable.data.some(row => row && row.length > 1 && row[1] === value);
};
should(summaryHasValue(testData.newCredentialName)).be.true;
should(summaryHasValue(testData.newDataSourceName)).be.true;
should(summaryHasValue(testData.externalTableInfoList[0].externalTableName.join('.'))).be.true;
// No value is included for the master key row, so check the row's key instead
should(summaryHasKey('Database Master Key')).be.true;
});
it('[SummaryPage Test] SummaryPage should register task operation on wizard submit.', async () => {
let uiElement = new SummaryUiElements();
uiElement.destDBLabel = new MockTextComponent();
uiElement.summaryTable = new MockDeclarativeTableComponent();
let env = new VirtualizeDataMockEnv();
let dataModel = env.getMockedVirtualizeDataModel();
let vdiManager = env.getMockedVDIManager();
let apiContext = env.getMockedAppContext();
let wizardMock = env.getWizardMock();
let summaryPage = new SummaryPage(dataModel, vdiManager, apiContext);
summaryPage.setUi(uiElement);
should(await summaryPage.validate()).be.true;
wizardMock.verify(x => x.registerOperation(TypeMoq.It.isAny()), TypeMoq.Times.once());
});
});

View File

@@ -0,0 +1,76 @@
/*---------------------------------------------------------------------------------------------
* 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 { ITreeNode } from './types';
type TreeNodePredicate = (node: TreeNode) => boolean;
export abstract class TreeNode implements ITreeNode {
private _parent: TreeNode = undefined;
public get parent(): TreeNode {
return this._parent;
}
public set parent(node: TreeNode) {
this._parent = node;
}
public generateNodePath(): string {
let path = undefined;
if (this.parent) {
path = this.parent.generateNodePath();
}
path = path ? `${path}/${this.nodePathValue}` : this.nodePathValue;
return path;
}
public findNodeByPath(path: string, expandIfNeeded: boolean = false): Promise<TreeNode> {
let condition: TreeNodePredicate = (node: TreeNode) => node.getNodeInfo().nodePath === path;
let filter: TreeNodePredicate = (node: TreeNode) => path.startsWith(node.getNodeInfo().nodePath);
return TreeNode.findNode(this, condition, filter, true);
}
public static async findNode(node: TreeNode, condition: TreeNodePredicate, filter: TreeNodePredicate, expandIfNeeded: boolean): Promise<TreeNode> {
if (!node) {
return undefined;
}
if (condition(node)) {
return node;
}
let nodeInfo = node.getNodeInfo();
if (nodeInfo.isLeaf) {
return undefined;
}
// TODO #659 support filtering by already expanded / not yet expanded
let children = await node.getChildren(false);
if (children) {
for (let child of children) {
if (filter && filter(child)) {
let childNode = await this.findNode(child, condition, filter, expandIfNeeded);
if (childNode) {
return childNode;
}
}
}
}
return undefined;
}
/**
* The value to use for this node in the node path
*/
public abstract get nodePathValue(): string;
abstract getChildren(refreshChildren: boolean): TreeNode[] | Promise<TreeNode[]>;
abstract getTreeItem(): vscode.TreeItem | Promise<vscode.TreeItem>;
abstract getNodeInfo(): azdata.NodeInfo;
}

View File

@@ -0,0 +1,63 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import * as azdata from 'azdata';
/**
* The API provided by this extension
*
* @export
* @interface IExtensionApi
*/
export interface IExtensionApi {
/**
* Gets the object explorer API that supports querying over the connections supported by this extension
*
* @returns {IObjectExplorerBrowser}
* @memberof IExtensionApi
*/
getObjectExplorerBrowser(): IObjectExplorerBrowser;
}
/**
* A browser supporting actions over the object explorer connections provided by this extension.
* Currently this is the
*
* @export
* @interface IObjectExplorerBrowser
*/
export interface IObjectExplorerBrowser {
/**
* Gets the matching node given a context object, e.g. one from a right-click on a node in Object Explorer
*
* @param {azdata.ObjectExplorerContext} objectExplorerContext
* @returns {Promise<T>}
*/
getNode<T extends ITreeNode>(objectExplorerContext: azdata.ObjectExplorerContext): Promise<T>;
}
/**
* A tree node in the object explorer tree
*
* @export
* @interface ITreeNode
*/
export interface ITreeNode {
getNodeInfo(): azdata.NodeInfo;
getChildren(refreshChildren: boolean): ITreeNode[] | Promise<ITreeNode[]>;
}
/**
* A HDFS file node. This is a leaf node in the object explorer tree, and its contents
* can be queried
*
* @export
* @interface IFileNode
* @extends {ITreeNode}
*/
export interface IFileNode extends ITreeNode {
getFileContentsAsString(maxBytes?: number): Promise<string>;
}

View File

@@ -0,0 +1,72 @@
// Generated by typings
// Source: https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/56295f5058cac7ae458540423c50ac2dcf9fc711/istanbul/istanbul.d.ts
declare module 'istanbul' {
namespace istanbul {
interface Istanbul {
new (options?: any): Istanbul;
Collector: Collector;
config: Config;
ContentWriter: ContentWriter;
FileWriter: FileWriter;
hook: Hook;
Instrumenter: Instrumenter;
Report: Report;
Reporter: Reporter;
Store: Store;
utils: ObjectUtils;
VERSION: string;
Writer: Writer;
}
interface Collector {
new (options?: any): Collector;
add(coverage: any, testName?: string): void;
}
interface Config {
}
interface ContentWriter {
}
interface FileWriter {
}
interface Hook {
hookRequire(matcher: any, transformer: any, options: any): void;
unhookRequire(): void;
}
interface Instrumenter {
new (options?: any): Instrumenter;
instrumentSync(code: string, filename: string): string;
}
interface Report {
}
interface Configuration {
new (obj: any, overrides: any): Configuration;
}
interface Reporter {
new (cfg?: Configuration, dir?: string): Reporter;
add(fmt: string): void;
addAll(fmts: Array<string>): void;
write(collector: Collector, sync: boolean, callback: Function): void;
}
interface Store {
}
interface ObjectUtils {
}
interface Writer {
}
}
var istanbul: istanbul.Istanbul;
export = istanbul;
}

View File

@@ -0,0 +1,8 @@
{
"resolution": "main",
"tree": {
"src": "https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/56295f5058cac7ae458540423c50ac2dcf9fc711/istanbul/istanbul.d.ts",
"raw": "registry:dt/istanbul#0.4.0+20160316155526",
"typings": "https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/56295f5058cac7ae458540423c50ac2dcf9fc711/istanbul/istanbul.d.ts"
}
}

View File

@@ -0,0 +1,5 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
declare module 'markdown-it-named-headers' { }

View File

@@ -0,0 +1,65 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// This is the place for extensions to expose APIs.
import * as azdata from 'azdata';
import * as vscode from 'vscode';
/**
* The APIs provided by Mssql extension
*
* @export
* @interface MssqlExtensionApi
*/
export interface MssqlExtensionApi {
/**
* Gets the object explorer API that supports querying over the connections supported by this extension
*
* @returns {IMssqlObjectExplorerBrowser}
* @memberof IMssqlExtensionApi
*/
getMssqlObjectExplorerBrowser(): MssqlObjectExplorerBrowser;
}
/**
* A browser supporting actions over the object explorer connections provided by this extension.
* Currently this is the
*
* @export
* @interface MssqlObjectExplorerBrowser
*/
export interface MssqlObjectExplorerBrowser {
/**
* Gets the matching node given a context object, e.g. one from a right-click on a node in Object Explorer
*
* @param {azdata.ObjectExplorerContext} objectExplorerContext
* @returns {Promise<T>}
*/
getNode<T extends ITreeNode>(objectExplorerContext: azdata.ObjectExplorerContext): Promise<T>;
}
/**
* A tree node in the object explorer tree
*
* @export
* @interface ITreeNode
*/
export interface ITreeNode {
getNodeInfo(): azdata.NodeInfo;
getChildren(refreshChildren: boolean): ITreeNode[] | Promise<ITreeNode[]>;
}
/**
* A HDFS file node. This is a leaf node in the object explorer tree, and its contents
* can be queried
*
* @export
* @interface IFileNode
* @extends {ITreeNode}
*/
export interface IFileNode extends ITreeNode {
getFileContentsAsString(maxBytes?: number): Promise<string>;
}

View File

@@ -0,0 +1,9 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/// <reference path='../../../../src/vscode-dts/vscode.d.ts'/>
/// <reference path='../../../../src/sql/azdata.d.ts'/>
/// <reference path='../../../../src/sql/azdata.proposed.d.ts'/>
/// <reference types='@types/node'/>

View File

@@ -0,0 +1,232 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as childProcess from 'child_process';
import { ExecOptions } from 'child_process';
import * as nls from 'vscode-nls';
import * as path from 'path';
import * as azdata from 'azdata';
import * as vscode from 'vscode';
import * as Constants from './constants';
import { CategoryValue } from 'azdata';
const localize = nls.loadMessageBundle();
export function getDropdownValue(dropdownValue: string | CategoryValue): string {
if (typeof (dropdownValue) === 'string') {
return <string>dropdownValue;
} else {
return dropdownValue ? (<CategoryValue>dropdownValue).name : undefined;
}
}
/**
* Helper to log messages to the developer console if enabled
* @param msg Message to log to the console
*/
export function logDebug(msg: any): void {
let config = vscode.workspace.getConfiguration(Constants.extensionConfigSectionName);
let logDebugInfo = config[Constants.configLogDebugInfo];
if (logDebugInfo === true) {
let currentTime = new Date().toLocaleTimeString();
let outputMsg = '[' + currentTime + ']: ' + msg ? msg.toString() : '';
console.log(outputMsg);
}
}
export function getKnoxUrl(host: string, port: string): string {
return `https://${host}:${port}/gateway`;
}
export function getLivyUrl(serverName: string, port: string): string {
return this.getKnoxUrl(serverName, port) + '/default/livy/v1/';
}
export function getTemplatePath(extensionPath: string, templateName: string): string {
return path.join(extensionPath, 'resources', templateName);
}
export function getErrorMessage(error: Error | string): string {
return (error instanceof Error) ? error.message : error;
}
// COMMAND EXECUTION HELPERS ///////////////////////////////////////////////
export function executeBufferedCommand(cmd: string, options: ExecOptions, outputChannel?: vscode.OutputChannel): Thenable<string> {
return new Promise<string>((resolve, reject) => {
if (outputChannel) {
outputChannel.appendLine(` > ${cmd}`);
}
let child = childProcess.exec(cmd, options, (err, stdout) => {
if (err) {
reject(err);
} else {
resolve(stdout);
}
});
// Add listeners to print stdout and stderr if an output channel was provided
if (outputChannel) {
child.stdout.on('data', data => { outputDataChunk(data, outputChannel, ' stdout: '); });
child.stderr.on('data', data => { outputDataChunk(data, outputChannel, ' stderr: '); });
}
});
}
export function executeExitCodeCommand(cmd: string, outputChannel?: vscode.OutputChannel): Thenable<number> {
return new Promise<number>((resolve, reject) => {
if (outputChannel) {
outputChannel.appendLine(` > ${cmd}`);
}
let child = childProcess.spawn(cmd, [], { shell: true, detached: false });
// Add listeners for the process to exit
child.on('error', reject);
child.on('exit', (code: number) => { resolve(code); });
// Add listeners to print stdout and stderr if an output channel was provided
if (outputChannel) {
child.stdout.on('data', data => { outputDataChunk(data, outputChannel, ' stdout: '); });
child.stderr.on('data', data => { outputDataChunk(data, outputChannel, ' stderr: '); });
}
});
}
export function executeStreamedCommand(cmd: string, outputChannel?: vscode.OutputChannel): Thenable<void> {
return new Promise<void>((resolve, reject) => {
// Start the command
if (outputChannel) {
outputChannel.appendLine(` > ${cmd}`);
}
let child = childProcess.spawn(cmd, [], { shell: true, detached: false });
// Add listeners to resolve/reject the promise on exit
child.on('error', reject);
child.on('exit', (code: number) => {
if (code === 0) {
resolve();
} else {
reject(localize('executeCommandProcessExited', 'Process exited with code {0}', code));
}
});
// Add listeners to print stdout and stderr if an output channel was provided
if (outputChannel) {
child.stdout.on('data', data => { outputDataChunk(data, outputChannel, ' stdout: '); });
child.stderr.on('data', data => { outputDataChunk(data, outputChannel, ' stderr: '); });
}
});
}
export function isObjectExplorerContext(object: any): object is azdata.ObjectExplorerContext {
return 'connectionProfile' in object && 'isConnectionNode' in object;
}
export function getUserHome(): string {
return process.env.HOME || process.env.USERPROFILE;
}
export enum Platform {
Mac,
Linux,
Windows,
Others
}
export function getOSPlatform(): Platform {
switch (process.platform) {
case 'win32':
return Platform.Windows;
case 'darwin':
return Platform.Mac;
case 'linux':
return Platform.Linux;
default:
return Platform.Others;
}
}
export function getOSPlatformId(): string {
var platformId = undefined;
switch (process.platform) {
case 'win32':
platformId = 'win-x64';
break;
case 'darwin':
platformId = 'osx';
break;
default:
platformId = 'linux-x64';
break;
}
return platformId;
}
// PRIVATE HELPERS /////////////////////////////////////////////////////////
function outputDataChunk(data: string | Buffer, outputChannel: vscode.OutputChannel, header: string): void {
data.toString().split(/\r?\n/)
.forEach(line => {
outputChannel.appendLine(header + line);
});
}
export function clone<T>(obj: T): T {
if (!obj || typeof obj !== 'object') {
return obj;
}
if (obj instanceof RegExp) {
// See https://github.com/Microsoft/TypeScript/issues/10990
return obj as any;
}
const result = (Array.isArray(obj)) ? <any>[] : <any>{};
Object.keys(obj).forEach(key => {
if (obj[key] && typeof obj[key] === 'object') {
result[key] = clone(obj[key]);
} else {
result[key] = obj[key];
}
});
return result;
}
export function isValidNumber(maybeNumber: any) {
return maybeNumber !== undefined
&& maybeNumber !== null
&& maybeNumber !== ''
&& !isNaN(Number(maybeNumber.toString()));
}
/**
* Removes the leading and trailing slashes from the pathName portion of a URL.
* @param pathName Path name portion of a URL.
* @returns Cleaned pathName string, or empty string if pathName is undefined.
*/
export function stripUrlPathSlashes(pathName: string): string {
if (!pathName) {
return '';
}
// Exclude empty trailing slashes
const lastCharIndex = pathName.length - 1;
if (pathName.length > 0 && pathName[lastCharIndex] === '/') {
let parseEndIndex = 0;
for (let i = lastCharIndex; i >= 0; --i) {
if (pathName[i] !== '/') {
parseEndIndex = i + 1;
break;
}
}
pathName = pathName.slice(0, parseEndIndex);
}
// Strip leading slash
if (pathName.length > 0 && pathName[0] === '/') {
pathName = pathName.slice(1);
}
return pathName;
}

View File

@@ -0,0 +1,38 @@
/*---------------------------------------------------------------------------------------------
* 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 { DataSourceWizardService } from '../../../services/contracts';
import { ImportDataModel } from './models';
import { TableFromFileWizard } from '../tableFromFileWizard';
export abstract class ImportPage {
protected constructor(
protected readonly instance: TableFromFileWizard,
protected readonly wizardPage: azdata.window.WizardPage,
protected readonly model: ImportDataModel,
protected readonly view: azdata.ModelView,
protected readonly provider: DataSourceWizardService) { }
/**
* This method constructs all the elements of the page.
* @returns {Promise<boolean>}
*/
public abstract start(): Promise<boolean>;
/**
* This method is called when the user is entering the page.
* @returns {Promise<boolean>}
*/
public abstract onPageEnter(): Promise<void>;
/**
* This method is called when the user is leaving the page.
* @returns {Promise<boolean>}
*/
public abstract onPageLeave(clickedNext: boolean): Promise<boolean>;
}

View File

@@ -0,0 +1,40 @@
/*---------------------------------------------------------------------------------------------
* 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 { FileNode } from '../../../hdfsProvider';
import { ColumnDefinition, DataSourceInstance } from '../../../services/contracts';
/**
* The main data model that communicates between the pages.
*/
export interface ImportDataModel {
proseColumns: ColumnDefinition[];
proseDataPreview: string[][];
serverConn: azdata.connection.ConnectionProfile;
sessionId: string;
allDatabases: string[];
versionInfo: {
serverMajorVersion: number;
productLevel: string;
};
database: string;
existingDataSource: string;
newDataSource: DataSourceInstance;
table: string;
fileFormat: string;
existingSchema: string;
newSchema: string;
parentFile: {
isFolder: boolean;
filePath: string;
};
proseParsingFile: FileNode;
fileType: string;
columnDelimiter: string;
firstRow: number;
quoteCharacter: string;
}

View File

@@ -0,0 +1,548 @@
/*---------------------------------------------------------------------------------------------
* 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 nls from 'vscode-nls';
import * as path from 'path';
import * as url from 'url';
import { ImportDataModel } from '../api/models';
import { ImportPage } from '../api/importPage';
import { TableFromFileWizard } from '../tableFromFileWizard';
import { DataSourceWizardService, DatabaseInfo } from '../../../services/contracts';
import { getDropdownValue, getErrorMessage, stripUrlPathSlashes } from '../../../utils';
import { ctp24Version, sql2019MajorVersion, ctp25Version, ctp3Version } from '../../../constants';
const localize = nls.loadMessageBundle();
export class FileConfigPageUiElements {
public fileTextBox: azdata.TextComponent;
public serverTextBox: azdata.TextComponent;
public databaseDropdown: azdata.DropDownComponent;
public dataSourceDropdown: azdata.DropDownComponent;
public tableNameTextBox: azdata.InputBoxComponent;
public schemaDropdown: azdata.DropDownComponent;
public databaseLoader: azdata.LoadingComponent;
public dataSourceLoader: azdata.LoadingComponent;
public schemaLoader: azdata.LoadingComponent;
public fileFormatNameTextBox: azdata.InputBoxComponent;
public refreshButton: azdata.ButtonComponent;
}
export class FileConfigPage extends ImportPage {
private ui: FileConfigPageUiElements;
public form: azdata.FormContainer;
private readonly noDataSourcesError = localize('tableFromFileImport.noDataSources', 'No valid external data sources were found in the specified database.');
private readonly noSchemasError = localize('tableFromFileImport.noSchemas', 'No user schemas were found in the specified database.');
private readonly tableExistsError = localize('tableFromFileImport.tableExists', 'The specified table name already exists under the specified schema.');
private readonly fileFormatExistsError = localize('tableFromFileImport.fileFormatExists', 'The specified external file format name already exists.');
private pageSetupComplete: boolean = false;
private existingTableSet: Set<string>;
private existingFileFormatSet: Set<string>;
private existingSchemaSet: Set<string>;
public constructor(instance: TableFromFileWizard, wizardPage: azdata.window.WizardPage, model: ImportDataModel, view: azdata.ModelView, provider: DataSourceWizardService) {
super(instance, wizardPage, model, view, provider);
}
public setUi(ui: FileConfigPageUiElements) {
this.ui = ui;
}
async start(): Promise<boolean> {
this.ui = new FileConfigPageUiElements();
let fileNameComponent = this.createFileTextBox();
let serverNameComponent = this.createServerTextBox();
let databaseComponent = this.createDatabaseDropdown();
let dataSourceComponent = this.createDataSourceDropdown();
let tableNameComponent = this.createTableNameBox();
let schemaComponent = this.createSchemaDropdown();
let fileFormatNameComponent = this.createFileFormatNameBox();
let refreshButton = this.createRefreshButton();
this.form = this.view.modelBuilder.formContainer()
.withFormItems([
fileNameComponent,
serverNameComponent,
databaseComponent,
dataSourceComponent,
tableNameComponent,
schemaComponent,
fileFormatNameComponent,
refreshButton
]).component();
await this.view.initializeModel(this.form);
return true;
}
async onPageEnter(): Promise<void> {
if (!this.pageSetupComplete) {
this.instance.clearStatusMessage();
this.toggleInputsEnabled(false, true);
try {
this.parseFileInfo();
await this.createSession();
await this.populateDatabaseDropdown();
await this.populateDatabaseInfo();
} finally {
this.toggleInputsEnabled(true, true);
}
this.pageSetupComplete = true;
}
}
async onPageLeave(clickedNext: boolean): Promise<boolean> {
if (this.ui.schemaLoader.loading ||
this.ui.databaseLoader.loading ||
this.ui.dataSourceLoader.loading ||
!this.ui.refreshButton.enabled) {
return false;
}
if (clickedNext) {
if ((this.model.newSchema === undefined || this.model.newSchema === '') &&
(this.model.existingSchema === undefined || this.model.existingSchema === '')) {
return false;
}
if (!this.model.newDataSource &&
(this.model.existingDataSource === undefined || this.model.existingDataSource === '')) {
return false;
}
if (this.model.existingSchema && this.model.existingSchema !== '' &&
this.existingTableSet && this.existingTableSet.has(this.model.existingSchema + '.' + this.model.table)) {
this.instance.showErrorMessage(this.tableExistsError);
return false;
}
if (this.existingFileFormatSet && this.existingFileFormatSet.has(this.model.fileFormat)) {
this.instance.showErrorMessage(this.fileFormatExistsError);
return false;
}
}
return true;
}
private async createSession(): Promise<void> {
try {
this.ui.serverTextBox.value = this.model.serverConn.serverName;
if (this.model.sessionId) {
await this.provider.disposeWizardSession(this.model.sessionId);
delete this.model.sessionId;
delete this.model.allDatabases;
delete this.model.versionInfo;
}
let sessionResponse = await this.provider.createDataSourceWizardSession(this.model.serverConn);
this.model.sessionId = sessionResponse.sessionId;
this.model.allDatabases = sessionResponse.databaseList.map(db => db.name);
this.model.versionInfo = {
serverMajorVersion: sessionResponse.serverMajorVersion,
productLevel: sessionResponse.productLevel
};
} catch (err) {
this.instance.showErrorMessage(getErrorMessage(err));
}
}
private createDatabaseDropdown(): azdata.FormComponent {
this.ui.databaseDropdown = this.view.modelBuilder.dropDown().withProps({
values: [''],
value: undefined
}).component();
// Handle database changes
this.ui.databaseDropdown.onValueChanged(async (db) => {
this.model.database = getDropdownValue(this.ui.databaseDropdown.value);
this.instance.clearStatusMessage();
this.toggleInputsEnabled(false, false);
try {
await this.populateDatabaseInfo();
} finally {
this.toggleInputsEnabled(true, false);
}
});
this.ui.databaseLoader = this.view.modelBuilder.loadingComponent().withItem(this.ui.databaseDropdown).component();
return {
component: this.ui.databaseLoader,
title: localize('tableFromFileImport.databaseDropdownTitle', 'Database the external table will be created in')
};
}
private async populateDatabaseDropdown(): Promise<boolean> {
let idx = -1;
let count = -1;
let dbNames = await this.model.allDatabases.map(dbName => {
count++;
if (this.model.database && dbName === this.model.database) {
idx = count;
}
return dbName;
});
if (idx >= 0) {
let tmp = dbNames[0];
dbNames[0] = dbNames[idx];
dbNames[idx] = tmp;
}
this.model.database = dbNames[0];
this.ui.databaseDropdown.updateProperties({
values: dbNames,
value: dbNames[0]
});
return true;
}
private createDataSourceDropdown(): azdata.FormComponent {
this.ui.dataSourceDropdown = this.view.modelBuilder.dropDown().withProps({
values: [''],
value: undefined
}).component();
this.ui.dataSourceDropdown.onValueChanged(async (db) => {
if (!this.model.newDataSource) {
this.model.existingDataSource = getDropdownValue(this.ui.dataSourceDropdown.value);
}
});
this.ui.dataSourceLoader = this.view.modelBuilder.loadingComponent().withItem(this.ui.dataSourceDropdown).component();
return {
component: this.ui.dataSourceLoader,
title: localize('tableFromFileImport.dataSourceDropdown', 'External data source for new external table')
};
}
private populateDataSourceDropdown(dbInfo: DatabaseInfo): boolean {
let errorCleanup = (errorMsg: string = this.noDataSourcesError) => {
this.ui.dataSourceDropdown.updateProperties({ values: [''], value: undefined });
this.instance.showErrorMessage(errorMsg);
this.model.existingDataSource = undefined;
this.model.newDataSource = undefined;
};
if (!dbInfo || !dbInfo.externalDataSources) {
errorCleanup();
return false;
}
let expectedDataSourceHost: string;
let expectedDataSourcePort: string;
let expectedDataSourcePath = '';
let majorVersion = this.model.versionInfo.serverMajorVersion;
let productLevel = this.model.versionInfo.productLevel;
if (majorVersion === sql2019MajorVersion && productLevel === ctp24Version) {
expectedDataSourceHost = 'service-master-pool';
expectedDataSourcePort = '50070';
} else if (majorVersion === sql2019MajorVersion && productLevel === ctp25Version) {
expectedDataSourceHost = 'nmnode-0-svc';
expectedDataSourcePort = '50070';
} else if (majorVersion === sql2019MajorVersion && productLevel === ctp3Version) {
expectedDataSourceHost = 'controller-svc';
expectedDataSourcePort = '8080';
expectedDataSourcePath = 'default';
} else { // Default: SQL 2019 CTP 3.1 syntax
expectedDataSourceHost = 'controller-svc';
expectedDataSourcePort = null;
expectedDataSourcePath = 'default';
}
let filteredSources = dbInfo.externalDataSources.filter(dataSource => {
if (!dataSource.location) {
return false;
}
let locationUrl = url.parse(dataSource.location);
let pathName = stripUrlPathSlashes(locationUrl.pathname);
return locationUrl.protocol === 'sqlhdfs:'
&& locationUrl.hostname === expectedDataSourceHost
&& locationUrl.port === expectedDataSourcePort
&& pathName === expectedDataSourcePath;
});
if (filteredSources.length === 0) {
let sourceName = 'SqlStoragePool';
let nameSuffix = 0;
let existingNames = new Set<string>(dbInfo.externalDataSources.map(dataSource => dataSource.name));
while (existingNames.has(sourceName)) {
sourceName = `SqlStoragePool${++nameSuffix}`;
}
let storageLocation: string;
if (expectedDataSourcePort !== null) {
storageLocation = `sqlhdfs://${expectedDataSourceHost}:${expectedDataSourcePort}/${expectedDataSourcePath}`;
} else {
storageLocation = `sqlhdfs://${expectedDataSourceHost}/${expectedDataSourcePath}`;
}
this.model.newDataSource = {
name: sourceName,
location: storageLocation,
authenticationType: undefined,
username: undefined,
credentialName: undefined
};
filteredSources.unshift(this.model.newDataSource);
} else {
this.model.newDataSource = undefined;
}
let idx = -1;
let count = -1;
let dataSourceNames = filteredSources.map(dataSource => {
let sourceName = dataSource.name;
count++;
if ((this.model.existingDataSource && sourceName === this.model.existingDataSource) ||
(this.model.newDataSource && sourceName === this.model.newDataSource.name)) {
idx = count;
}
return sourceName;
});
if (idx >= 0) {
let tmp = dataSourceNames[0];
dataSourceNames[0] = dataSourceNames[idx];
dataSourceNames[idx] = tmp;
}
if (this.model.newDataSource) {
this.model.existingDataSource = undefined;
} else {
this.model.existingDataSource = dataSourceNames[0];
}
this.ui.dataSourceDropdown.updateProperties({
values: dataSourceNames,
value: dataSourceNames[0]
});
return true;
}
private createFileTextBox(): azdata.FormComponent {
this.ui.fileTextBox = this.view.modelBuilder.text().component();
let title = this.model.parentFile.isFolder
? localize('tableFromFileImport.folderTextboxTitle', 'Source Folder')
: localize('tableFromFileImport.fileTextboxTitle', 'Source File');
return {
component: this.ui.fileTextBox,
title: title
};
}
private createServerTextBox(): azdata.FormComponent {
this.ui.serverTextBox = this.view.modelBuilder.text().component();
return {
component: this.ui.serverTextBox,
title: localize('tableFromFileImport.destConnTitle', 'Destination Server')
};
}
private parseFileInfo(): void {
let parentFilePath = this.model.parentFile.filePath;
this.ui.fileTextBox.value = parentFilePath;
let parsingFileExtension = path.extname(this.model.proseParsingFile.hdfsPath);
if (parsingFileExtension.toLowerCase() === '.json') {
this.model.fileType = 'JSON';
} else {
this.model.fileType = 'TXT';
}
let parentBaseName = path.basename(parentFilePath, parsingFileExtension);
this.ui.tableNameTextBox.value = parentBaseName;
this.model.table = this.ui.tableNameTextBox.value;
this.ui.tableNameTextBox.validate();
this.ui.fileFormatNameTextBox.value = `FileFormat_${parentBaseName}`;
this.model.fileFormat = this.ui.fileFormatNameTextBox.value;
this.ui.fileFormatNameTextBox.validate();
}
private createTableNameBox(): azdata.FormComponent {
this.ui.tableNameTextBox = this.view.modelBuilder.inputBox()
.withValidation((name) => {
let tableName = name.value;
if (!tableName || tableName.length === 0) {
return false;
}
return true;
}).withProperties({
required: true,
}).component();
this.ui.tableNameTextBox.onTextChanged((tableName) => {
this.model.table = tableName;
});
return {
component: this.ui.tableNameTextBox,
title: localize('tableFromFileImport.tableTextboxTitle', 'Name for new external table '),
};
}
private createFileFormatNameBox(): azdata.FormComponent {
this.ui.fileFormatNameTextBox = this.view.modelBuilder.inputBox()
.withValidation((name) => {
let fileFormat = name.value;
if (!fileFormat || fileFormat.length === 0) {
return false;
}
return true;
}).withProperties({
required: true,
}).component();
this.ui.fileFormatNameTextBox.onTextChanged((fileFormat) => {
this.model.fileFormat = fileFormat;
});
return {
component: this.ui.fileFormatNameTextBox,
title: localize('tableFromFileImport.fileFormatTextboxTitle', 'Name for new table\'s external file format'),
};
}
private createSchemaDropdown(): azdata.FormComponent {
this.ui.schemaDropdown = this.view.modelBuilder.dropDown().withProps({
values: [''],
value: undefined,
editable: true,
fireOnTextChange: true
}).component();
this.ui.schemaLoader = this.view.modelBuilder.loadingComponent().withItem(this.ui.schemaDropdown).component();
this.ui.schemaDropdown.onValueChanged(() => {
let schema = getDropdownValue(this.ui.schemaDropdown.value);
if (this.existingSchemaSet.has(schema)) {
this.model.newSchema = undefined;
this.model.existingSchema = schema;
} else {
this.model.newSchema = schema;
this.model.existingSchema = undefined;
}
});
return {
component: this.ui.schemaLoader,
title: localize('tableFromFileImport.schemaTextboxTitle', 'Schema for new external table'),
};
}
private populateSchemaDropdown(dbInfo: DatabaseInfo): boolean {
if (!dbInfo || !dbInfo.schemaList || dbInfo.schemaList.length === 0) {
this.ui.schemaDropdown.updateProperties({ values: [''], value: undefined });
this.instance.showErrorMessage(this.noSchemasError);
this.model.newSchema = undefined;
this.model.existingSchema = undefined;
return false;
}
this.model.newSchema = undefined;
if (!this.model.existingSchema) {
this.model.existingSchema = dbInfo.defaultSchema;
}
let idx = -1;
let count = -1;
let values = dbInfo.schemaList.map(schema => {
count++;
if (this.model.existingSchema && schema === this.model.existingSchema) {
idx = count;
}
return schema;
});
if (idx >= 0) {
let tmp = values[0];
values[0] = values[idx];
values[idx] = tmp;
} else {
// Default schema wasn't in the list, so take the first one instead
this.model.existingSchema = values[0];
}
this.ui.schemaDropdown.updateProperties({
values: values,
value: values[0]
});
return true;
}
private async refreshPage(): Promise<void> {
this.pageSetupComplete = false;
await this.onPageEnter();
}
private createRefreshButton(): azdata.FormComponent {
this.ui.refreshButton = this.view.modelBuilder.button().withProps({
label: localize('tableFromFileImport.refreshButtonTitle', 'Refresh')
}).component();
this.ui.refreshButton.onDidClick(async () => await this.refreshPage());
return {
component: this.ui.refreshButton,
title: undefined
};
}
private async populateDatabaseInfo(): Promise<boolean> {
try {
let dbInfo: DatabaseInfo = undefined;
let dbInfoResponse = await this.provider.getDatabaseInfo({ sessionId: this.model.sessionId, databaseName: this.model.database });
if (!dbInfoResponse.isSuccess) {
this.instance.showErrorMessage(dbInfoResponse.errorMessages.join('\n'));
this.existingTableSet = undefined;
this.existingFileFormatSet = undefined;
this.existingSchemaSet = undefined;
} else {
dbInfo = dbInfoResponse.databaseInfo;
this.existingTableSet = new Set<string>(dbInfo.externalTables.map(table => table.schemaName + '.' + table.tableName));
this.existingFileFormatSet = new Set<string>(dbInfo.externalFileFormats);
this.existingSchemaSet = new Set<string>(dbInfo.schemaList);
}
let r1 = this.populateDataSourceDropdown(dbInfo);
let r2 = this.populateSchemaDropdown(dbInfo);
return r1 && r2;
} catch (err) {
this.instance.showErrorMessage(getErrorMessage(err));
}
}
private toggleInputsEnabled(enable: boolean, includeDbLoader: boolean) {
if (includeDbLoader) {
this.ui.databaseLoader.loading = !enable;
}
this.ui.databaseDropdown.enabled = enable;
this.ui.refreshButton.enabled = enable;
this.ui.dataSourceDropdown.enabled = enable;
this.ui.schemaDropdown.enabled = enable;
this.ui.dataSourceLoader.loading = !enable;
this.ui.schemaLoader.loading = !enable;
}
}

View File

@@ -0,0 +1,154 @@
/*---------------------------------------------------------------------------------------------
* 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 nls from 'vscode-nls';
import { ImportDataModel } from '../api/models';
import { ImportPage } from '../api/importPage';
import { TableFromFileWizard } from '../tableFromFileWizard';
import { DataSourceWizardService, ColumnDefinition } from '../../../services/contracts';
const localize = nls.loadMessageBundle();
export class ModifyColumnsPageUiElements {
public table: azdata.DeclarativeTableComponent;
public loading: azdata.LoadingComponent;
public text: azdata.TextComponent;
}
export class ModifyColumnsPage extends ImportPage {
private readonly categoryValues = [
{ name: 'bigint', displayName: 'bigint' },
{ name: 'binary(50)', displayName: 'binary(50)' },
{ name: 'bit', displayName: 'bit' },
{ name: 'char(10)', displayName: 'char(10)' },
{ name: 'date', displayName: 'date' },
{ name: 'datetime', displayName: 'datetime' },
{ name: 'datetime2(7)', displayName: 'datetime2(7)' },
{ name: 'datetimeoffset(7)', displayName: 'datetimeoffset(7)' },
{ name: 'decimal(18, 10)', displayName: 'decimal(18, 10)' },
{ name: 'float', displayName: 'float' },
{ name: 'geography', displayName: 'geography' },
{ name: 'geometry', displayName: 'geometry' },
{ name: 'hierarchyid', displayName: 'hierarchyid' },
{ name: 'int', displayName: 'int' },
{ name: 'money', displayName: 'money' },
{ name: 'nchar(10)', displayName: 'nchar(10)' },
{ name: 'ntext', displayName: 'ntext' },
{ name: 'numeric(18, 0)', displayName: 'numeric(18, 0)' },
{ name: 'nvarchar(50)', displayName: 'nvarchar(50)' },
{ name: 'nvarchar(MAX)', displayName: 'nvarchar(MAX)' },
{ name: 'real', displayName: 'real' },
{ name: 'smalldatetime', displayName: 'smalldatetime' },
{ name: 'smallint', displayName: 'smallint' },
{ name: 'smallmoney', displayName: 'smallmoney' },
{ name: 'sql_variant', displayName: 'sql_variant' },
{ name: 'text', displayName: 'text' },
{ name: 'time(7)', displayName: 'time(7)' },
{ name: 'timestamp', displayName: 'timestamp' },
{ name: 'tinyint', displayName: 'tinyint' },
{ name: 'uniqueidentifier', displayName: 'uniqueidentifier' },
{ name: 'varbinary(50)', displayName: 'varbinary(50)' },
{ name: 'varbinary(MAX)', displayName: 'varbinary(MAX)' },
{ name: 'varchar(50)', displayName: 'varchar(50)' },
{ name: 'varchar(MAX)', displayName: 'varchar(MAX)' }
];
private ui: ModifyColumnsPageUiElements;
private form: azdata.FormContainer;
public constructor(instance: TableFromFileWizard, wizardPage: azdata.window.WizardPage, model: ImportDataModel, view: azdata.ModelView, provider: DataSourceWizardService) {
super(instance, wizardPage, model, view, provider);
}
public setUi(ui: ModifyColumnsPageUiElements) {
this.ui = ui;
}
private static convertMetadata(column: ColumnDefinition): any[] {
return [column.columnName, column.dataType, column.isNullable];
}
async start(): Promise<boolean> {
this.ui = new ModifyColumnsPageUiElements();
this.ui.loading = this.view.modelBuilder.loadingComponent().component();
this.ui.table = this.view.modelBuilder.declarativeTable().component();
this.ui.text = this.view.modelBuilder.text().component();
this.ui.table.onDataChanged((e) => {
this.model.proseColumns = [];
this.ui.table.data.forEach((row) => {
this.model.proseColumns.push({
columnName: row[0],
dataType: row[1],
isNullable: row[2],
collationName: undefined
});
});
});
this.form = this.view.modelBuilder.formContainer()
.withFormItems(
[
{
component: this.ui.text,
title: ''
},
{
component: this.ui.table,
title: ''
}
], {
horizontal: false,
componentWidth: '100%'
}).component();
this.ui.loading.component = this.form;
await this.view.initializeModel(this.form);
return true;
}
async onPageEnter(): Promise<void> {
this.ui.loading.loading = true;
await this.populateTable();
this.ui.loading.loading = false;
}
async onPageLeave(clickedNext: boolean): Promise<boolean> {
if (this.ui.loading.loading) {
return false;
}
return true;
}
private async populateTable() {
let data: any[][] = [];
this.model.proseColumns.forEach((column) => {
data.push(ModifyColumnsPage.convertMetadata(column));
});
this.ui.table.updateProperties({
columns: [{
displayName: localize('tableFromFileImport.columnName', 'Column Name'),
valueType: azdata.DeclarativeDataType.string,
width: '150px',
isReadOnly: false
}, {
displayName: localize('tableFromFileImport.dataType', 'Data Type'),
valueType: azdata.DeclarativeDataType.editableCategory,
width: '150px',
isReadOnly: false,
categoryValues: this.categoryValues
}, {
displayName: localize('tableFromFileImport.allowNulls', 'Allow Nulls'),
valueType: azdata.DeclarativeDataType.boolean,
isReadOnly: false,
width: '100px'
}],
data: data
});
}
}

View File

@@ -0,0 +1,159 @@
/*---------------------------------------------------------------------------------------------
* 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 nls from 'vscode-nls';
import * as vscode from 'vscode';
import { ImportDataModel } from '../api/models';
import { ImportPage } from '../api/importPage';
import { TableFromFileWizard } from '../tableFromFileWizard';
import { DataSourceWizardService, ColumnDefinition, ProseDiscoveryResponse } from '../../../services/contracts';
import { getErrorMessage } from '../../../utils';
import { extensionConfigSectionName, configProseParsingMaxLines, proseMaxLinesDefault } from '../../../constants';
const localize = nls.loadMessageBundle();
export class ProsePreviewPageUiElements {
public table: azdata.TableComponent;
public loading: azdata.LoadingComponent;
}
export class ProsePreviewPage extends ImportPage {
private ui: ProsePreviewPageUiElements;
private form: azdata.FormContainer;
private proseParsingComplete: Promise<ProseDiscoveryResponse>;
public constructor(instance: TableFromFileWizard, wizardPage: azdata.window.WizardPage, model: ImportDataModel, view: azdata.ModelView, provider: DataSourceWizardService) {
super(instance, wizardPage, model, view, provider);
this.proseParsingComplete = this.doProseDiscovery();
}
public setUi(ui: ProsePreviewPageUiElements) {
this.ui = ui;
}
async start(): Promise<boolean> {
this.ui = new ProsePreviewPageUiElements();
this.ui.table = this.view.modelBuilder.table().component();
this.ui.loading = this.view.modelBuilder.loadingComponent().component();
this.form = this.view.modelBuilder.formContainer().withFormItems([
{
component: this.ui.table,
title: localize('tableFromFileImport.prosePreviewMessage', 'This operation analyzed the input file structure to generate the preview below for up to the first 50 rows.')
}
]).component();
this.ui.loading.component = this.form;
await this.view.initializeModel(this.ui.loading);
return true;
}
async onPageEnter(): Promise<void> {
if (!this.model.proseDataPreview) {
this.ui.loading.loading = true;
await this.handleProsePreview();
this.ui.loading.loading = false;
await this.populateTable(this.model.proseDataPreview, this.model.proseColumns);
}
}
async onPageLeave(clickedNext: boolean): Promise<boolean> {
if (this.ui.loading.loading) {
return false;
}
if (clickedNext) {
// Should have shown an error for these already in the loading step
return this.model.proseDataPreview !== undefined && this.model.proseColumns !== undefined;
} else {
return true;
}
}
private async doProseDiscovery(): Promise<ProseDiscoveryResponse> {
let maxLines = proseMaxLinesDefault;
let config = vscode.workspace.getConfiguration(extensionConfigSectionName);
if (config) {
let maxLinesConfig = config[configProseParsingMaxLines];
if (maxLinesConfig) {
maxLines = maxLinesConfig;
}
}
let contents = await this.model.proseParsingFile.getFileLinesAsString(maxLines);
return this.provider.sendProseDiscoveryRequest({
filePath: undefined,
tableName: this.model.table,
schemaName: this.model.newSchema ? this.model.newSchema : this.model.existingSchema,
fileType: this.model.fileType,
fileContents: contents
});
}
private async handleProsePreview() {
let result: ProseDiscoveryResponse;
try {
result = await this.proseParsingComplete;
} catch (err) {
this.instance.showErrorMessage(getErrorMessage(err));
return;
}
if (!result || !result.dataPreview) {
this.instance.showErrorMessage(localize('tableFromFileImport.noPreviewData', 'Failed to retrieve any data from the specified file.'));
return;
}
if (!result.columnInfo) {
this.instance.showErrorMessage(localize('tableFromFileImport.noProseInfo', 'Failed to generate column information for the specified file.'));
return;
}
this.model.proseDataPreview = result.dataPreview;
this.model.proseColumns = [];
result.columnInfo.forEach((column) => {
this.model.proseColumns.push({
columnName: column.name,
dataType: column.sqlType,
isNullable: column.isNullable,
collationName: undefined
});
});
let unquoteString = (value: string): string => {
return value ? value.replace(/^"(.*)"$/, '$1') : undefined;
};
this.model.columnDelimiter = unquoteString(result.columnDelimiter);
this.model.firstRow = result.firstRow;
this.model.quoteCharacter = unquoteString(result.quoteCharacter);
}
private async populateTable(tableData: string[][], columns: ColumnDefinition[]) {
let columnHeaders: string[] = columns ? columns.map(c => c.columnName) : undefined;
let rows;
const maxRows = 50;
if (tableData && tableData.length > maxRows) {
rows = tableData.slice(0, maxRows);
} else {
rows = tableData;
}
this.ui.table.updateProperties({
data: rows,
columns: columnHeaders,
height: 600,
width: 800
});
}
}

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.
*--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata';
import * as nls from 'vscode-nls';
import { ImportDataModel } from '../api/models';
import { ImportPage } from '../api/importPage';
import { TableFromFileWizard } from '../tableFromFileWizard';
import { DataSourceWizardService } from '../../../services/contracts';
const localize = nls.loadMessageBundle();
export class SummaryPageUiElements {
public table: azdata.TableComponent;
}
export class SummaryPage extends ImportPage {
private ui: SummaryPageUiElements;
private form: azdata.FormContainer;
public constructor(instance: TableFromFileWizard, wizardPage: azdata.window.WizardPage, model: ImportDataModel, view: azdata.ModelView, provider: DataSourceWizardService) {
super(instance, wizardPage, model, view, provider);
}
public setUi(ui: SummaryPageUiElements) {
this.ui = ui;
}
async start(): Promise<boolean> {
this.ui = new SummaryPageUiElements();
this.ui.table = this.view.modelBuilder.table().component();
this.form = this.view.modelBuilder.formContainer().withFormItems(
[{
component: this.ui.table,
title: localize('tableFromFileImport.importInformation', 'Data Virtualization information')
}]
).component();
await this.view.initializeModel(this.form);
return true;
}
async onPageEnter(): Promise<void> {
this.instance.changeDoneButtonLabel(localize('tableFromFileImport.importData', 'Virtualize Data'));
this.instance.setGenerateScriptVisibility(true);
this.populateTable();
}
async onPageLeave(clickedNext: boolean): Promise<boolean> {
this.instance.changeDoneButtonLabel(localize('tableFromFileImport.next', 'Next'));
this.instance.setGenerateScriptVisibility(false);
return true;
}
private populateTable() {
let sourceTitle = this.model.parentFile.isFolder
? localize('tableFromFileImport.summaryFolderName', 'Source Folder')
: localize('tableFromFileImport.summaryFileName', 'Source File');
this.ui.table.updateProperties({
data: [
[localize('tableFromFileImport.serverName', 'Server name'), this.model.serverConn.serverName],
[localize('tableFromFileImport.databaseName', 'Database name'), this.model.database],
[localize('tableFromFileImport.tableName', 'Table name'), this.model.table],
[localize('tableFromFileImport.tableSchema', 'Table schema'), this.model.newSchema ? this.model.newSchema : this.model.existingSchema],
[localize('tableFromFileImport.fileFormat', 'File format name'), this.model.fileFormat],
[sourceTitle, this.model.parentFile.filePath]
],
columns: ['Object type', 'Name'],
width: 600,
height: 200
});
}
}

View File

@@ -0,0 +1,329 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
import * as azdata from 'azdata';
import * as path from 'path';
import * as url from 'url';
import * as utils from '../../utils';
import { ImportDataModel } from './api/models';
import { ImportPage } from './api/importPage';
import { FileConfigPage } from './pages/fileConfigPage';
import { ProsePreviewPage } from './pages/prosePreviewPage';
import { ModifyColumnsPage } from './pages/modifyColumnsPage';
import { SummaryPage } from './pages/summaryPage';
import { DataSourceWizardService, VirtualizeDataInput } from '../../services/contracts';
import { HdfsFileSourceNode, FileNode } from '../../hdfsProvider';
import { AppContext } from '../../appContext';
import { TreeNode } from '../../treeNodes';
import { HdfsItems, MssqlClusterItems, DataSourceType, delimitedTextFileType } from '../../constants';
const localize = nls.loadMessageBundle();
export class TableFromFileWizard {
private readonly connection: azdata.connection.ConnectionProfile;
private readonly appContext: AppContext;
private readonly provider: DataSourceWizardService;
private wizard: azdata.window.Wizard;
private model: ImportDataModel;
constructor(connection: azdata.connection.ConnectionProfile, appContext: AppContext, provider: DataSourceWizardService) {
this.connection = connection;
this.appContext = appContext;
this.provider = provider;
}
public async start(hdfsFileNode: HdfsFileSourceNode, ...args: any[]) {
if (!hdfsFileNode) {
vscode.window.showErrorMessage(localize('import.needFile', 'Please select a source file or folder before using this wizard.'));
return;
}
let noCsvError = localize('tableFromFileImport.onlyCsvSupported', 'Currently only csv files are supported for this wizard.');
let proseParsingFile: FileNode;
let parentIsFolder = false;
let isFolder = (node: TreeNode): boolean => {
let nodeType = node.getNodeInfo().nodeType;
return nodeType === HdfsItems.Folder || nodeType === MssqlClusterItems.Folder;
};
if (isFolder(hdfsFileNode)) {
let visibleFilesFilter = node => {
// Polybase excludes files that start with '.' or '_', so skip these
// files when trying to find a file to run prose discovery on
if (node.hdfsPath) {
let baseName = path.basename(node.hdfsPath);
return baseName.length > 0 && baseName[0] !== '.' && baseName[0] !== '_';
}
return false;
};
let nodeSearch = async (condition) => TreeNode.findNode(hdfsFileNode, condition, visibleFilesFilter, true);
let nonCsvFile = await nodeSearch(node => {
return !isFolder(node) && path.extname(node.hdfsPath).toLowerCase() !== '.csv';
});
if (nonCsvFile) {
vscode.window.showErrorMessage(noCsvError);
return;
}
let csvFile = await nodeSearch(node => {
return !isFolder(node) && path.extname(node.hdfsPath).toLowerCase() === '.csv';
}) as FileNode;
if (!csvFile) {
vscode.window.showErrorMessage(localize('tableFromFileImport.noCsvFileFound', 'No csv files were found in the specified folder.'));
return;
}
parentIsFolder = true;
proseParsingFile = csvFile;
} else {
if (path.extname(hdfsFileNode.hdfsPath).toLowerCase() !== '.csv') {
vscode.window.showErrorMessage(noCsvError);
return;
}
proseParsingFile = hdfsFileNode as FileNode;
}
this.model = <ImportDataModel>{
parentFile: {
isFolder: parentIsFolder,
filePath: hdfsFileNode.hdfsPath
},
proseParsingFile: proseParsingFile,
serverConn: this.connection
};
let pages: Map<number, ImportPage> = new Map<number, ImportPage>();
this.wizard = azdata.window.createWizard(localize('tableFromFileImport.wizardName', 'Virtualize Data From CSV'));
let page0 = azdata.window.createWizardPage(localize('tableFromFileImport.page0Name', 'Select the destination database for your external table'));
let page1 = azdata.window.createWizardPage(localize('tableFromFileImport.page1Name', 'Preview Data'));
let page2 = azdata.window.createWizardPage(localize('tableFromFileImport.page2Name', 'Modify Columns'));
let page3 = azdata.window.createWizardPage(localize('tableFromFileImport.page3Name', 'Summary'));
let fileConfigPage: FileConfigPage;
page0.registerContent(async (view) => {
fileConfigPage = new FileConfigPage(this, page0, this.model, view, this.provider);
pages.set(0, fileConfigPage);
await fileConfigPage.start().then(() => {
fileConfigPage.onPageEnter();
});
});
let prosePreviewPage: ProsePreviewPage;
page1.registerContent(async (view) => {
prosePreviewPage = new ProsePreviewPage(this, page1, this.model, view, this.provider);
pages.set(1, prosePreviewPage);
await prosePreviewPage.start();
});
let modifyColumnsPage: ModifyColumnsPage;
page2.registerContent(async (view) => {
modifyColumnsPage = new ModifyColumnsPage(this, page2, this.model, view, this.provider);
pages.set(2, modifyColumnsPage);
await modifyColumnsPage.start();
});
let summaryPage: SummaryPage;
page3.registerContent(async (view) => {
summaryPage = new SummaryPage(this, page3, this.model, view, this.provider);
pages.set(3, summaryPage);
await summaryPage.start();
});
this.wizard.onPageChanged(async info => {
let newPage = pages.get(info.newPage);
if (newPage) {
await newPage.onPageEnter();
}
});
this.wizard.registerNavigationValidator(async (info) => {
let lastPage = pages.get(info.lastPage);
let newPage = pages.get(info.newPage);
// Hit "next" on last page, so handle submit
let nextOnLastPage = !newPage && lastPage instanceof SummaryPage;
if (nextOnLastPage) {
let createSuccess = await this.handleVirtualizeData();
if (createSuccess) {
this.showTaskComplete();
}
return createSuccess;
}
if (lastPage) {
let clickedNext = nextOnLastPage || info.newPage > info.lastPage;
let pageValid = await lastPage.onPageLeave(clickedNext);
if (!pageValid) {
return false;
}
}
this.clearStatusMessage();
return true;
});
let cleanupSession = async () => {
try {
if (this.model.sessionId) {
await this.provider.disposeWizardSession(this.model.sessionId);
delete this.model.sessionId;
delete this.model.allDatabases;
}
} catch (error) {
this.appContext.apiWrapper.showErrorMessage(error.toString());
}
};
this.wizard.cancelButton.onClick(() => {
cleanupSession();
});
this.wizard.doneButton.onClick(() => {
cleanupSession();
});
this.wizard.generateScriptButton.hidden = true;
this.wizard.generateScriptButton.onClick(async () => {
let input = TableFromFileWizard.generateInputFromModel(this.model);
let generateScriptResponse = await this.provider.generateScript(input);
if (generateScriptResponse.isSuccess) {
let doc = await this.appContext.apiWrapper.openTextDocument({ language: 'sql', content: generateScriptResponse.script });
await this.appContext.apiWrapper.showDocument(doc);
this.showInfoMessage(
localize('tableFromFileImport.openScriptMsg',
'The script has opened in a document window. You can view it once the wizard is closed.'));
} else {
this.showErrorMessage(generateScriptResponse.errorMessages.join('\n'));
}
});
this.wizard.pages = [page0, page1, page2, page3];
this.wizard.open();
}
public setGenerateScriptVisibility(visible: boolean) {
this.wizard.generateScriptButton.hidden = !visible;
}
public registerNavigationValidator(validator: (pageChangeInfo: azdata.window.WizardPageChangeInfo) => boolean) {
this.wizard.registerNavigationValidator(validator);
}
public changeDoneButtonLabel(label: string) {
this.wizard.doneButton.label = label;
}
public showErrorMessage(errorMsg: string) {
this.showStatusMessage(errorMsg, azdata.window.MessageLevel.Error);
}
public showInfoMessage(infoMsg: string) {
this.showStatusMessage(infoMsg, azdata.window.MessageLevel.Information);
}
private async getConnectionInfo(): Promise<azdata.connection.ConnectionProfile> {
let serverConn = await azdata.connection.getCurrentConnection();
if (serverConn) {
let credentials = await azdata.connection.getCredentials(serverConn.connectionId);
if (credentials) {
Object.assign(serverConn, credentials);
}
}
return serverConn;
}
private showStatusMessage(message: string, level: azdata.window.MessageLevel) {
this.wizard.message = <azdata.window.DialogMessage>{
text: message,
level: level
};
}
public clearStatusMessage() {
this.wizard.message = undefined;
}
public static generateInputFromModel(model: ImportDataModel): VirtualizeDataInput {
if (!model) {
return undefined;
}
let result = <VirtualizeDataInput>{
sessionId: model.sessionId,
destDatabaseName: model.database,
sourceServerType: DataSourceType.SqlHDFS,
externalTableInfoList: [{
externalTableName: undefined,
columnDefinitionList: model.proseColumns,
sourceTableLocation: [model.parentFile.filePath],
fileFormat: {
formatName: model.fileFormat,
formatType: delimitedTextFileType,
fieldTerminator: model.columnDelimiter,
stringDelimiter: model.quoteCharacter,
firstRow: model.firstRow
}
}]
};
if (model.newDataSource) {
result.newDataSourceName = model.newDataSource.name;
let dataSrcUrl = url.parse(model.newDataSource.location);
result.sourceServerName = `${dataSrcUrl.host}${dataSrcUrl.pathname}`;
} else {
result.existingDataSourceName = model.existingDataSource;
}
if (model.newSchema) {
result.newSchemas = [model.newSchema];
result.externalTableInfoList[0].externalTableName = [model.newSchema, model.table];
} else {
result.externalTableInfoList[0].externalTableName = [model.existingSchema, model.table];
}
return result;
}
private async handleVirtualizeData(): Promise<boolean> {
let errorMsg: string;
try {
let dataInput = TableFromFileWizard.generateInputFromModel(this.model);
let createTableResponse = await this.provider.processVirtualizeDataInput(dataInput);
if (!createTableResponse.isSuccess) {
errorMsg = createTableResponse.errorMessages.join('\n');
}
} catch (err) {
errorMsg = utils.getErrorMessage(err);
}
if (errorMsg) {
this.showErrorMessage(errorMsg);
return false;
}
return true;
}
private showTaskComplete() {
this.wizard.registerOperation({
connection: undefined,
displayName: localize('tableFromFile.taskLabel', 'Virtualize Data'),
description: undefined,
isCancelable: false,
operation: op => {
op.updateStatus(azdata.TaskStatus.Succeeded);
}
});
}
}

View File

@@ -0,0 +1,312 @@
/*---------------------------------------------------------------------------------------------
* 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 nls from 'vscode-nls';
const localize = nls.loadMessageBundle();
import { IWizardPageWrapper } from '../wizardPageWrapper';
import { VirtualizeDataModel } from './virtualizeDataModel';
import { VirtualizeDataInput } from '../../services/contracts';
import { getDropdownValue } from '../../utils';
import { AppContext } from '../../appContext';
import { VDIManager } from './virtualizeDataInputManager';
import { dataSourcePrefixMapping, connectionPageInfoMapping } from '../../constants';
export class ConnectionDetailsPage implements IWizardPageWrapper {
private _page: azdata.window.WizardPage;
private _modelBuilder: azdata.ModelBuilder;
private _mainContainer: azdata.FlexContainer;
private _dataSourceNameForm: azdata.FormComponent;
private _sourceServerInfoComponentsFormGroup: azdata.FormComponentGroup;
private _credentialComponentsFormGroup: azdata.FormComponentGroup;
private _dataSourceNameDropDown: azdata.DropDownComponent;
private _serverNameInput: azdata.InputBoxComponent;
private _databaseNameInput: azdata.InputBoxComponent;
private _existingCredDropdown: azdata.DropDownComponent;
private _credentialNameInput: azdata.InputBoxComponent;
private _usernameInput: azdata.InputBoxComponent;
private _passwordInput: azdata.InputBoxComponent;
private readonly _createCredLabel = localize('newCredOption', '-- Create New Credential --');
private readonly _parentLayout: azdata.FormItemLayout = { horizontal: true, componentWidth: '600px' };
private readonly _dataSourceNameInputBoxLayout: azdata.FormItemLayout =
Object.assign({ info: localize('dataSourceHelpText', 'The name for your External Data Source.') }, this._parentLayout);
private readonly _existingCredDropdownLayout: azdata.FormItemLayout =
Object.assign({
info: localize('credNameHelpText',
'The name of the Database Scoped Credential used to securely store the login information for the External Data Source you are creating.')
}, this._parentLayout);
private _currentDataSourceType: string;
private _currentDestDbName: string;
constructor(private _dataModel: VirtualizeDataModel, private _vdiManager: VDIManager, private _appContext: AppContext) {
this._page = this._appContext.apiWrapper.createWizardPage(localize('connectionDetailsTitle', 'Create a connection to your Data Source'));
this._page.registerContent(async (modelView) => {
this._modelBuilder = modelView.modelBuilder;
this._mainContainer = this._modelBuilder.flexContainer().component();
await modelView.initializeModel(this._mainContainer);
});
}
public async buildMainContainer(): Promise<void> {
// Create data source fields first, since it preloads the database metadata
await this.buildDataSourceNameForm();
await this.buildSourceServerInfoComponentsFormGroup();
await this.buildCredentialComponentsFormGroup();
const serverAndCredentialComponents: (azdata.FormComponent | azdata.FormComponentGroup)[] = [];
serverAndCredentialComponents.push(this._sourceServerInfoComponentsFormGroup);
serverAndCredentialComponents.push(this._credentialComponentsFormGroup);
const mainFormBuilder: azdata.FormBuilder = this._modelBuilder.formContainer();
mainFormBuilder.addFormItem(this._dataSourceNameForm, this._dataSourceNameInputBoxLayout);
mainFormBuilder.addFormItems(serverAndCredentialComponents, this._parentLayout);
this._mainContainer.clearItems();
this._mainContainer.addItem(mainFormBuilder.component());
}
public async buildDataSourceNameForm(): Promise<void> {
let destinationDB = this._vdiManager.destinationDatabaseName;
let dbInfo = await this._dataModel.loadDatabaseInfo(destinationDB);
let existingDataSources = dbInfo ? dbInfo.externalDataSources : [];
const locationPrefix = dataSourcePrefixMapping.get(this._currentDataSourceType) ?? '';
existingDataSources = existingDataSources.filter(ds => ds.location.startsWith(locationPrefix));
let dataSourceInfo = existingDataSources.map(e => {
return { name: e.name, location: e.location, credName: e.credentialName };
});
this._dataSourceNameDropDown = this._modelBuilder.dropDown().component();
await this._dataSourceNameDropDown.updateProperties({
values: [''].concat(dataSourceInfo.map(e => `${e.name} (${e.location}, ${e.credName})`)),
value: undefined,
editable: true,
height: undefined,
enabled: true,
fireOnTextChange: true
});
this._dataSourceNameDropDown.onValueChanged(async () => {
let dataSourceName = getDropdownValue(this._dataSourceNameDropDown.value);
let dsInfo = dataSourceInfo.find(e => dataSourceName === `${e.name} (${e.location}, ${e.credName})`);
if (dsInfo) {
await this._dataSourceNameDropDown.updateProperties({ value: dsInfo.name });
return;
}
if (dataSourceName === '') {
await this._dataSourceNameDropDown.updateProperties({ value: undefined });
await this.toggleServerCredInputs(true, '', this._createCredLabel, '', '', '');
return;
}
let selectedDataSource = existingDataSources.find(ds => ds.name === this._dataSourceNameDropDown.value);
if (selectedDataSource) {
let serverName: string = selectedDataSource.location.substring(locationPrefix.length);
await this.toggleServerCredInputs(false, serverName, selectedDataSource.credentialName,
selectedDataSource.credentialName, selectedDataSource.username, '');
return;
}
if (!this._serverNameInput.enabled) {
await this.toggleServerCredInputs(true, '', this._createCredLabel, '', '', '');
return;
}
});
this._dataSourceNameForm = <azdata.FormComponent>{
component: this._dataSourceNameDropDown,
title: localize('sourceNameInput', 'External Data Source Name'),
required: true
};
}
public async toggleServerCredInputs(
enable: boolean,
serverNameValue: string,
credDropDownValue: string,
credNameValue: string,
usernameValue: string,
passwordValue: string
): Promise<void> {
// There is a bug in recognizing required field.
// As workaround, it intentionally updates 'enabled' property first and then update 'value'
await this._serverNameInput.updateProperties({ enabled: enable });
await this._existingCredDropdown.updateProperties({ enabled: enable });
await this._credentialNameInput.updateProperties({ enabled: enable });
await this._usernameInput.updateProperties({ enabled: enable });
await this._passwordInput.updateProperties({ enabled: enable });
await this._serverNameInput.updateProperties({ value: serverNameValue });
await this._existingCredDropdown.updateProperties({ value: credDropDownValue });
await this._credentialNameInput.updateProperties({ value: credNameValue });
await this._usernameInput.updateProperties({ value: usernameValue });
await this._passwordInput.updateProperties({ value: passwordValue });
}
// Server-specific fields
public async buildSourceServerInfoComponentsFormGroup(): Promise<void> {
let serverNameValue: string = '';
let dbNameValue: string = '';
const connectionPageInfo = connectionPageInfoMapping.get(this._currentDataSourceType);
let sourceServerInfoComponents: azdata.FormComponent[] = [];
this._serverNameInput = this._modelBuilder.inputBox().withProps({
value: serverNameValue
}).component();
sourceServerInfoComponents.push({
component: this._serverNameInput,
title: connectionPageInfo.serverNameTitle,
required: true
});
this._databaseNameInput = this._modelBuilder.inputBox().withProps({
value: dbNameValue
}).component();
sourceServerInfoComponents.push({
component: this._databaseNameInput,
title: connectionPageInfo.databaseNameTitle,
required: connectionPageInfo.isDbRequired
});
this._sourceServerInfoComponentsFormGroup = {
components: sourceServerInfoComponents,
title: localize('serverFields', 'Server Connection')
};
}
// Credential fields
public async buildCredentialComponentsFormGroup(): Promise<void> {
let credentialNames = this._dataModel.existingCredentials ?
this._dataModel.existingCredentials.map(cred => cred.credentialName) : [];
credentialNames.unshift(this._createCredLabel);
let credDropDownValues: string[] = credentialNames;
let credDropDownValue: string = this._createCredLabel;
let credDropDownRequired: boolean = true;
let credNameValue: string = '';
let credNameRequired: boolean = true;
let usernameValue: string = '';
let usernameRequired: boolean = true;
let passwordValue: string = '';
let passwordRequired: boolean = true;
let credentialComponents: (azdata.FormComponent & { layout?: azdata.FormItemLayout })[] = [];
this._existingCredDropdown = this._modelBuilder.dropDown().withProps({
values: credDropDownValues,
value: credDropDownValue,
}).component();
this._existingCredDropdown.onValueChanged(async (selection) => {
if (selection.selected === this._createCredLabel) {
await this.toggleCredentialInputs(true);
} else {
await this.toggleCredentialInputs(false);
await this._credentialNameInput.updateProperties({ value: '' });
let credential = this._dataModel.existingCredentials.find(cred => cred.credentialName === selection.selected);
await this._usernameInput.updateProperties({ value: credential ? credential.username : '' });
await this._passwordInput.updateProperties({ value: '' });
}
});
credentialComponents.push({
component: this._existingCredDropdown,
title: localize('credentialNameDropdown', 'Choose Credential'),
required: credDropDownRequired,
layout: this._existingCredDropdownLayout
});
this._credentialNameInput = this._modelBuilder.inputBox().withProps({
value: credNameValue,
}).component();
credentialComponents.push({
component: this._credentialNameInput,
title: localize('credentialNameInput', 'New Credential Name'),
required: credNameRequired
});
this._usernameInput = this._modelBuilder.inputBox().withProps({
value: usernameValue,
}).component();
credentialComponents.push({
component: this._usernameInput,
title: localize('usernameInput', 'Username'),
required: usernameRequired
});
this._passwordInput = this._modelBuilder.inputBox().withProps({
value: passwordValue,
inputType: 'password'
}).component();
credentialComponents.push({
component: this._passwordInput,
title: localize('passwordInput', 'Password'),
required: passwordRequired
});
this._credentialComponentsFormGroup = {
components: credentialComponents,
title: localize('credentialFields', 'Configure Credential')
};
}
public async validate(): Promise<boolean> {
let inputValues = this._vdiManager.getVirtualizeDataInput(this);
return this._dataModel.validateInput(inputValues);
}
public getPage(): azdata.window.WizardPage {
return this._page;
}
public async updatePage(): Promise<void> {
let newDataSourceType = this._vdiManager.sourceServerType;
let newDestDbName = this._vdiManager.destinationDatabaseName;
if ((newDataSourceType && this._currentDataSourceType !== newDataSourceType)
|| (newDestDbName && this._currentDestDbName !== newDestDbName)) {
this._currentDataSourceType = newDataSourceType;
this._currentDestDbName = newDestDbName;
await this.buildMainContainer();
}
}
private async toggleCredentialInputs(enable: boolean): Promise<void> {
await this._credentialNameInput.updateProperties({ enabled: enable });
await this._usernameInput.updateProperties({ enabled: enable });
await this._passwordInput.updateProperties({ enabled: enable });
}
public getInputValues(existingInput: VirtualizeDataInput): void {
if (!this._dataSourceNameDropDown) { return; }
let isNewDataSource: boolean = this._serverNameInput ? this._serverNameInput.enabled : undefined;
let dataSourceName: string = this._dataSourceNameDropDown ? getDropdownValue(this._dataSourceNameDropDown.value) : undefined;
if (isNewDataSource) {
existingInput.newDataSourceName = dataSourceName;
let isNewCredential: boolean = this._existingCredDropdown ?
this._existingCredDropdown.value === this._createCredLabel : undefined;
if (isNewCredential) {
existingInput.newCredentialName = this._credentialNameInput ? this._credentialNameInput.value : undefined;
existingInput.sourceUsername = this._usernameInput ? this._usernameInput.value : undefined;
existingInput.sourcePassword = this._passwordInput ? this._passwordInput.value : undefined;
} else {
existingInput.existingCredentialName = this._existingCredDropdown ?
getDropdownValue(this._existingCredDropdown.value) : undefined;
}
} else {
existingInput.existingDataSourceName = dataSourceName;
existingInput.existingCredentialName = this._existingCredDropdown ?
getDropdownValue(this._existingCredDropdown.value) : undefined;
}
existingInput.sourceServerName = this._serverNameInput ? this._serverNameInput.value : undefined;
existingInput.sourceDatabaseName = this._databaseNameInput ? this._databaseNameInput.value : undefined;
}
}

View File

@@ -0,0 +1,123 @@
/*---------------------------------------------------------------------------------------------
* 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 nls from 'vscode-nls';
const localize = nls.loadMessageBundle();
import { IWizardPageWrapper } from '../wizardPageWrapper';
import { VirtualizeDataModel } from './virtualizeDataModel';
import { VirtualizeDataInput } from '../../services/contracts';
import { VDIManager } from './virtualizeDataInputManager';
import { AppContext } from '../../appContext';
export class MasterKeyUiElements {
public masterKeyPasswordInput: azdata.InputBoxComponent;
public masterKeyPasswordConfirmInput: azdata.InputBoxComponent;
}
export class CreateMasterKeyPage implements IWizardPageWrapper {
private _page: azdata.window.WizardPage;
private _uiElements: MasterKeyUiElements;
private readonly _masterKeyExistsMsg = localize('masterKeyExistsMsg', 'A Master Key already exists for the selected database. No action is required on this page.');
public constructor(private _dataModel: VirtualizeDataModel, private _vdiManager: VDIManager, private _appContext: AppContext) {
this.buildPage();
}
public setUi(ui: MasterKeyUiElements): void {
this._uiElements = ui;
}
private buildPage(): void {
this._page = this._appContext.apiWrapper.createWizardPage(localize('createMasterKeyTitle', 'Create Database Master Key'));
this._page.description = localize(
'createMasterKeyDescription',
'A master key is required. This secures the credentials used by an External Data Source. Note that you should back up the master key by using BACKUP MASTER KEY and store the backup in a secure, off-site location.');
this._page.registerContent(async (modelView) => {
let ui = new MasterKeyUiElements();
let builder = modelView.modelBuilder;
let allComponents: (azdata.FormComponent | azdata.FormComponentGroup)[] = [];
// Master key fields
ui.masterKeyPasswordInput = builder.inputBox().withProperties({
inputType: 'password'
}).component();
ui.masterKeyPasswordConfirmInput = builder.inputBox().withProperties({
inputType: 'password'
}).component();
allComponents.push({
components:
[
{
component: ui.masterKeyPasswordInput,
title: localize('masterKeyPasswordInput', 'Password'),
required: true
},
{
component: ui.masterKeyPasswordConfirmInput,
title: localize('masterKeyPasswordConfirmInput', 'Confirm Password'),
required: true
}
],
title: localize('masterKeyPasswordLabel', 'Set the Master Key password.')
});
let formContainer = builder.formContainer()
.withFormItems(allComponents,
{
horizontal: true,
componentWidth: '600px'
}).component();
let pwdReminderText = builder.text().withProperties({
value: localize('pwdReminderText', 'Strong passwords use a combination of alphanumeric, upper, lower, and special characters.')
}).component();
let flexContainer = builder.flexContainer().withLayout({
flexFlow: 'column',
alignItems: 'stretch',
height: '100%',
width: '100%'
}).component();
flexContainer.addItem(formContainer, { CSSStyles: { 'padding': '0px' } });
flexContainer.addItem(pwdReminderText, { CSSStyles: { 'padding': '10px 0 0 30px' } });
this.setUi(ui);
await modelView.initializeModel(flexContainer);
});
}
public async validate(): Promise<boolean> {
if (this._uiElements.masterKeyPasswordInput.value === this._uiElements.masterKeyPasswordConfirmInput.value) {
let inputValues = this._vdiManager.getVirtualizeDataInput(this);
return this._dataModel.validateInput(inputValues);
} else {
this._dataModel.showWizardError(localize('passwordMismatchWithConfirmError', 'Password values do not match.'));
return false;
}
}
public getPage(): azdata.window.WizardPage {
return this._page;
}
public async updatePage(): Promise<void> {
let hasMasterKey: boolean = await this._dataModel.hasMasterKey();
this._uiElements.masterKeyPasswordInput.updateProperties({ enabled: !hasMasterKey, required: !hasMasterKey });
this._uiElements.masterKeyPasswordConfirmInput.updateProperties({ enabled: !hasMasterKey, required: !hasMasterKey });
if (hasMasterKey) {
this._dataModel.showWizardInfo(this._masterKeyExistsMsg);
}
}
public getInputValues(existingInput: VirtualizeDataInput): void {
existingInput.destDbMasterKeyPwd = (this._uiElements && this._uiElements.masterKeyPasswordInput) ?
this._uiElements.masterKeyPasswordInput.value : undefined;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,225 @@
/*---------------------------------------------------------------------------------------------
* 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 nls from 'vscode-nls';
import * as localizedConstants from '../../localizedConstants';
const localize = nls.loadMessageBundle();
import { IWizardPageWrapper } from '../wizardPageWrapper';
import { VirtualizeDataModel } from './virtualizeDataModel';
import { VirtualizeDataInput } from '../../services/contracts';
import { getDropdownValue } from '../../utils';
import { AppContext } from '../../appContext';
import { VDIManager } from './virtualizeDataInputManager';
import { VirtualizeDataWizard } from './virtualizeDataWizard';
import { CreateMasterKeyPage } from './createMasterKeyPage';
export class SelectDataSourcePage implements IWizardPageWrapper {
private readonly SqlServerType = localizedConstants.SqlServerName;
private readonly DefaultType = localize('defaultSourceType', 'Default');
private readonly IconsConfig: {} = {};
private _dataModel: VirtualizeDataModel;
private _vdiManager: VDIManager;
private _appContext: AppContext;
private _page: azdata.window.WizardPage;
private _modelBuilder: azdata.ModelBuilder;
private _formContainer: azdata.FormBuilder;
private _loadingSpinner: azdata.LoadingComponent;
private _destDBDropDown: azdata.DropDownComponent;
private _selectedSourceType: string;
private _componentsAreSetup: boolean;
private _modelInitialized: boolean;
constructor(private _virtualizeDataWizard: VirtualizeDataWizard) {
if (this._virtualizeDataWizard) {
this._dataModel = _virtualizeDataWizard.dataModel;
this._vdiManager = _virtualizeDataWizard.vdiManager;
this._appContext = _virtualizeDataWizard.appContext;
}
this._componentsAreSetup = false;
this._modelInitialized = false;
this.IconsConfig[this.SqlServerType] = {
light: 'resources/light/server.svg',
dark: 'resources/dark/server_inverse.svg'
};
this.IconsConfig[this.DefaultType] = {
light: 'resources/light/database.svg',
dark: 'resources/dark/database_inverse.svg'
};
this._page = azdata.window.createWizardPage(localize('selectDataSrcTitle', 'Select a Data Source'));
this._page.registerContent(async (modelView) => {
this._modelBuilder = modelView.modelBuilder;
this._formContainer = this._modelBuilder.formContainer();
let parentLayout: azdata.FormItemLayout = {
horizontal: false
};
this._destDBDropDown = this._modelBuilder.dropDown().withProps({
values: [],
value: '',
height: undefined,
width: undefined
}).component();
this._loadingSpinner = this._modelBuilder.loadingComponent()
.withItem(this._destDBDropDown)
.withProps({ loading: true })
.component();
this._formContainer.addFormItem({
component: this._loadingSpinner,
title: localize('destDBLabel', 'Select the destination database for your external table')
},
Object.assign({ info: localize('destDBHelpText', 'The database in which to create your External Data Source.') },
parentLayout)
);
await modelView.initializeModel(this._formContainer.component());
this._modelInitialized = true;
await this.setupPageComponents();
});
}
public async setupPageComponents(): Promise<void> {
if (!this._componentsAreSetup && this._modelInitialized && this._dataModel.configInfoResponse) {
this._componentsAreSetup = true;
let parentLayout: azdata.FormItemLayout = {
horizontal: false
};
// Destination DB
let databaseList: string[] = this._dataModel.destDatabaseList.map(db => db.name).sort((a, b) => a.localeCompare(b));
let connectedDatabase = this._dataModel.connection.databaseName;
let selectedDatabase: string;
if (connectedDatabase && databaseList.some(name => name === connectedDatabase)) {
selectedDatabase = connectedDatabase;
} else {
selectedDatabase = databaseList.length > 0 ? databaseList[0] : '';
}
await this._destDBDropDown.updateProperties({
values: databaseList,
value: selectedDatabase
});
await this.toggleCreateMasterKeyPage(getDropdownValue(this._destDBDropDown.value));
this._destDBDropDown.onValueChanged(async (selection) => {
await this.toggleCreateMasterKeyPage(selection.selected);
});
await this._loadingSpinner.updateProperties({
loading: false
});
// Source Type
let components: azdata.FormComponent[] = [];
let info = this._dataModel.configInfoResponse;
const cards: azdata.RadioCard[] = [];
info.supportedSourceTypes.forEach(sourceType => {
let typeName = sourceType.typeName;
let iconTypeName: string;
if (this.IconsConfig[typeName]) {
iconTypeName = typeName;
} else {
iconTypeName = this.DefaultType;
}
let iconPath = this._appContext ?
{
light: this._appContext.extensionContext.asAbsolutePath(this.IconsConfig[iconTypeName].light),
dark: this._appContext.extensionContext.asAbsolutePath(this.IconsConfig[iconTypeName].dark)
} : undefined;
cards.push({
id: typeName,
descriptions: [{ textValue: typeName }],
icon: iconPath
});
});
const cardGroup = this._modelBuilder.radioCardGroup().withProps({
cards: cards,
cardWidth: '150px',
cardHeight: '160px',
iconWidth: '50px',
iconHeight: '50px'
}).component();
cardGroup.onSelectionChanged((e: azdata.RadioCardSelectionChangedEvent) => {
this._selectedSourceType = e.cardId;
});
if (cards.length > 0) {
cardGroup.selectedCardId = cards[0].id;
}
components.push({
component: cardGroup,
title: localize('sourceCardsLabel', 'Select your data source type')
});
this._formContainer.addFormItems(components, parentLayout);
this._dataModel.wizard.nextButton.enabled = true;
}
}
public async validate(): Promise<boolean> {
let inputValues = this._vdiManager.getVirtualizeDataInput(this);
if (!inputValues.sourceServerType) {
this._dataModel.showWizardError(localize('noServerTypeError', 'A data source type must be selected.'));
return false;
}
if (!inputValues.destDatabaseName) {
this._dataModel.showWizardError(localize('noDestDatabaseError', 'A destination database must be selected.'));
return false;
}
return await this._dataModel.validateInput(inputValues);
}
private async toggleCreateMasterKeyPage(dbSelected: string): Promise<void> {
if (!dbSelected || !this._virtualizeDataWizard || !this._virtualizeDataWizard.wizard
|| !this._virtualizeDataWizard.wizard.pages) { return; }
let databaseListWithMasterKey: string[] = this._dataModel.destDatabaseList.filter(db => db.hasMasterKey).map(db => db.name) || [];
let currentPages = this._virtualizeDataWizard.wizard.pages;
let currentWrappers = currentPages.map(p => p['owner']);
if (databaseListWithMasterKey.find(e => e === dbSelected)) {
let indexToRemove = currentWrappers.findIndex(w => w instanceof CreateMasterKeyPage);
if (indexToRemove >= 0) {
await this._virtualizeDataWizard.wizard.removePage(indexToRemove);
}
} else if (!currentWrappers.find(w => w instanceof CreateMasterKeyPage)) {
let thisWrapperIndex = currentWrappers.findIndex(w => Object.is(w, this));
let createMasterKeyPageWrapper = this._virtualizeDataWizard.wizardPageWrappers.find(w => w instanceof CreateMasterKeyPage);
await this._virtualizeDataWizard.wizard.addPage(createMasterKeyPageWrapper.getPage(), thisWrapperIndex + 1);
}
}
public getPage(): azdata.window.WizardPage {
return this._page;
}
public async updatePage(): Promise<void> {
return;
}
public getInputValues(existingInput: VirtualizeDataInput): void {
existingInput.destDatabaseName = (this._destDBDropDown && this._destDBDropDown.value) ?
getDropdownValue(this._destDBDropDown.value) : undefined;
existingInput.sourceServerType = this._selectedSourceType;
}
}

View File

@@ -0,0 +1,200 @@
/*---------------------------------------------------------------------------------------------
* 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 nls from 'vscode-nls';
const localize = nls.loadMessageBundle();
import { IWizardPageWrapper } from '../wizardPageWrapper';
import { VirtualizeDataModel } from './virtualizeDataModel';
import { VirtualizeDataInput } from '../../services/contracts';
import { VDIManager } from './virtualizeDataInputManager';
import { AppContext } from '../../appContext';
export class SummaryUiElements {
public destDBLabel: azdata.TextComponent;
public summaryTable: azdata.DeclarativeTableComponent;
}
export class SummaryPage implements IWizardPageWrapper {
private _page: azdata.window.WizardPage;
private _uiElements: SummaryUiElements;
private readonly _taskLabel = localize('virtualizeTaskLabel', 'Virtualize Data');
constructor(private _dataModel: VirtualizeDataModel, private _vdiManager: VDIManager, private _appContext: AppContext) {
this._page = this._appContext.apiWrapper.createWizardPage(localize('summaryPageTitle', 'Summary'));
this._page.registerContent(async (modelView) => {
let ui = new SummaryUiElements();
let builder = modelView.modelBuilder;
let components: azdata.FormComponent[] = [];
ui.destDBLabel = builder.text().withProperties({
value: ''
}).component();
components.push({
component: ui.destDBLabel,
title: localize('summaryDestDb', 'Destination Database:')
});
let tableData = [['', '']];
ui.summaryTable = builder.declarativeTable()
.withProperties({
columns: [{
displayName: localize('summaryObjTypeLabel', 'Object type'),
valueType: azdata.DeclarativeDataType.string,
width: '300px',
isReadOnly: true
}, {
displayName: localize('summaryObjNameLabel', 'Name'),
valueType: azdata.DeclarativeDataType.string,
width: '300px',
isReadOnly: true
}
],
data: tableData
}).component();
components.push({
component: ui.summaryTable,
title: localize('summaryTitle', 'The following objects will be created in the destination database:')
});
let form = builder.formContainer()
.withFormItems(components, {
horizontal: false
})
.withLayout({
width: '800px'
}).component();
this.setUi(ui);
await modelView.initializeModel(form);
});
}
public setUi(ui: SummaryUiElements): void {
this._uiElements = ui;
}
public async validate(): Promise<boolean> {
this._dataModel.wizard.registerOperation({
connection: undefined,
displayName: this._taskLabel,
description: this._taskLabel,
isCancelable: false,
operation: op => {
op.updateStatus(azdata.TaskStatus.InProgress, localize('virtualizeTaskStart', 'Executing script...'));
let inputValues = this._vdiManager.getVirtualizeDataInput();
this._dataModel.processInput(inputValues).then(response => {
if (!response.isSuccess) {
op.updateStatus(azdata.TaskStatus.Failed, localize('createSourceError', 'External Table creation failed'));
if (response.errorMessages) {
this._appContext.apiWrapper.showErrorMessage(response.errorMessages.join('\n'));
}
} else {
op.updateStatus(azdata.TaskStatus.Succeeded, localize('createSourceInfo', 'External Table creation completed successfully'));
let serverName = this._dataModel.connection.serverName;
let databaseName = inputValues.destDatabaseName;
let nodePath = `${serverName}/Databases/${databaseName}/Tables`;
let username = this._dataModel.connection.userName;
SummaryPage.refreshExplorerNode(nodePath, '/', username);
}
});
}
});
// Always return true, so that wizard closes.
return true;
}
private static async refreshExplorerNode(nodePath: string, delimiter: string, username?: string): Promise<boolean> {
if (!nodePath || !delimiter) { return false; }
let refreshNodePath = nodePath.split(delimiter);
if (!refreshNodePath || refreshNodePath.length === 0) { return false; }
let isSuccess: boolean = false;
try {
let targetNodes: azdata.objectexplorer.ObjectExplorerNode[] = undefined;
let nodes = await azdata.objectexplorer.getActiveConnectionNodes();
if (nodes && username) {
nodes = nodes.filter(n => n.label.endsWith(` - ${username})`));
}
let currentNodePath: string = undefined;
for (let i = 0; i < refreshNodePath.length; ++i) {
if (nodes && nodes.length > 0) {
currentNodePath = currentNodePath ? `${currentNodePath}/${refreshNodePath[i]}` : refreshNodePath[i];
let currentNodes = nodes.filter(node => node.nodePath === currentNodePath);
if (currentNodes && currentNodes.length > 0) {
targetNodes = currentNodes;
let newNodes = [];
for (let n of targetNodes) { newNodes = newNodes.concat(await n.getChildren()); }
nodes = newNodes;
} else {
nodes = undefined;
}
} else {
break;
}
}
if (targetNodes && targetNodes.length > 0) {
for (let n of targetNodes) { await n.refresh(); }
isSuccess = true;
}
} catch { }
return isSuccess;
}
public getPage(): azdata.window.WizardPage {
return this._page;
}
public async updatePage(): Promise<void> {
let summary = this._vdiManager.getVirtualizeDataInput();
if (summary) {
await this._uiElements.destDBLabel.updateProperties({
value: summary.destDatabaseName
});
let tableData = this.getTableData(summary);
await this._uiElements.summaryTable.updateProperties({
data: tableData
});
}
}
private getTableData(summary: VirtualizeDataInput): string[][] {
let data = [];
if (summary.destDbMasterKeyPwd) {
let mdash = '\u2014';
data.push([localize('summaryMasterKeyLabel', 'Database Master Key'), mdash]);
}
if (summary.newCredentialName) {
data.push([localize('summaryCredLabel', 'Database Scoped Credential'), summary.newCredentialName]);
}
if (summary.newDataSourceName) {
data.push([localize('summaryDataSrcLabel', 'External Data Source'), summary.newDataSourceName]);
}
if (summary.newSchemas) {
for (let schemaName of summary.newSchemas) {
data.push([localize('summaryNewSchemaLabel', 'Schema'), schemaName]);
}
}
if (summary.externalTableInfoList) {
let labelText: string = localize('summaryExternalTableLabel', 'External Table');
for (let tableInfo of summary.externalTableInfoList) {
data.push([labelText, tableInfo.externalTableName.join('.')]);
}
}
return data;
}
public getInputValues(existingInput: VirtualizeDataInput): void {
return;
}
}

View File

@@ -0,0 +1,171 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ConnectionDetailsPage } from "./connectionDetailsPage";
import { CreateMasterKeyPage } from "./createMasterKeyPage";
import { IWizardPageWrapper } from "../wizardPageWrapper";
import { ObjectMappingPage } from "./objectMappingPage";
import { SelectDataSourcePage } from "./selectDataSourcePage";
import { VirtualizeDataModel } from "./virtualizeDataModel";
import { VirtualizeDataInput } from "../../services/contracts";
export class VDIManager {
private _selectDataSourcePage: IWizardPageWrapper;
private _createMasterKeyPage: IWizardPageWrapper;
private _connectionDetailsPage: IWizardPageWrapper;
private _objectMappingPage: IWizardPageWrapper;
private _pages: IWizardPageWrapper[];
private _virtualizeDataModel: VirtualizeDataModel;
private _propertyLookUp: Map<string, IWizardPageWrapper> = new Map<string, IWizardPageWrapper>();
public setInputPages(inputPages: IWizardPageWrapper[]): void {
if (inputPages && inputPages.length > 0) {
this._pages = inputPages;
this.setInputPagesInOrder();
this.setPropertyLookUp();
}
}
private setInputPagesInOrder(): void {
this._selectDataSourcePage = this.getSelectDataSourcePage();
this._createMasterKeyPage = this.getCreateMasterKeyPage();
this._connectionDetailsPage = this.getConnectionDetailsPage();
this._objectMappingPage = this.getObjectMappingPage();
let inputPages: IWizardPageWrapper[] = [];
[
this._selectDataSourcePage,
this._createMasterKeyPage,
this._connectionDetailsPage,
this._objectMappingPage
].forEach(e => {
if (e) { inputPages.push(e); }
});
this._pages = inputPages;
}
private setPropertyLookUp(): void {
if (this._pages && this._pages.length > 0) {
this._pages.forEach(page => {
if (page instanceof SelectDataSourcePage) {
this._propertyLookUp.set('destDatabaseName', page);
this._propertyLookUp.set('sourceServerType', page);
} else if (page instanceof CreateMasterKeyPage) {
this._propertyLookUp.set('destDbMasterKeyPwd', page);
} else if (page instanceof ConnectionDetailsPage) {
this._propertyLookUp.set('existingCredentialName', page);
this._propertyLookUp.set('newCredentialName', page);
this._propertyLookUp.set('sourceUsername', page);
this._propertyLookUp.set('sourcePassword', page);
this._propertyLookUp.set('existingDataSourceName', page);
this._propertyLookUp.set('newDataSourceName', page);
this._propertyLookUp.set('sourceServerName', page);
this._propertyLookUp.set('sourceDatabaseName', page);
} else if (page instanceof ObjectMappingPage) {
this._propertyLookUp.set('externalTableInfoList', page);
}
// No inputs set from SummaryPage
});
}
}
public setVirtualizeDataModel(virtualizeDataModel: VirtualizeDataModel): void {
this._virtualizeDataModel = virtualizeDataModel;
}
public getVirtualizeDataInput(upToPage?: IWizardPageWrapper): VirtualizeDataInput {
let virtualizeDataInput: VirtualizeDataInput = VDIManager.getEmptyInputInstance();
if (this._virtualizeDataModel && this._virtualizeDataModel.configInfoResponse) {
virtualizeDataInput.sessionId = this._virtualizeDataModel.configInfoResponse.sessionId;
}
for (let page of this._pages) {
if (page) {
page.getInputValues(virtualizeDataInput);
if (upToPage && page === upToPage) { break; }
}
}
return virtualizeDataInput;
}
public get virtualizeDataInput(): VirtualizeDataInput {
return this.getVirtualizeDataInput();
}
public getPropertyValue(property: string): any {
let propertyValue: any = undefined;
if (property && this._propertyLookUp.has(property)) {
let pageInput = VDIManager.getEmptyInputInstance();
this._propertyLookUp.get(property).getInputValues(pageInput);
if (pageInput) {
propertyValue = pageInput[property];
}
}
return propertyValue;
}
public get dataSourceName(): string {
return this.existingDataSourceName || this.newDataSourceName;
}
public get existingDataSourceName(): string {
return this.getPropertyValue('existingDataSourceName');
}
public get newDataSourceName(): string {
return this.getPropertyValue('newDataSourceName');
}
public get sourceServerName(): string {
return this.getPropertyValue('sourceServerName');
}
public get sourceDatabaseName(): string {
return this.getPropertyValue('sourceDatabaseName');
}
public get destinationDatabaseName(): string {
return this.getPropertyValue('destDatabaseName');
}
public get sourceServerType(): string {
return this.getPropertyValue('sourceServerType');
}
public get externalTableInfoList(): string {
return this.getPropertyValue('externalTableInfoList');
}
public get destDbMasterKeyPwd(): string {
return this.getPropertyValue('destDbMasterKeyPwd');
}
public get inputUptoConnectionDetailsPage(): VirtualizeDataInput {
let inputValues: VirtualizeDataInput = undefined;
if (this._connectionDetailsPage) {
inputValues = this.getVirtualizeDataInput(this._connectionDetailsPage);
}
return inputValues;
}
private getSelectDataSourcePage(): IWizardPageWrapper {
return this._pages.find(page => page instanceof SelectDataSourcePage);
}
private getCreateMasterKeyPage(): IWizardPageWrapper {
return this._pages.find(page => page instanceof CreateMasterKeyPage);
}
private getConnectionDetailsPage(): IWizardPageWrapper {
return this._pages.find(page => page instanceof ConnectionDetailsPage);
}
private getObjectMappingPage(): IWizardPageWrapper {
return this._pages.find(page => page instanceof ObjectMappingPage);
}
public static getEmptyInputInstance(): VirtualizeDataInput {
return <VirtualizeDataInput>{};
}
}

View File

@@ -0,0 +1,271 @@
/*---------------------------------------------------------------------------------------------
* 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 utils from '../../utils';
import {
DataSourceWizardConfigInfoResponse, DataSourceWizardService, VirtualizeDataInput,
ProcessVirtualizeDataInputResponse,
GenerateScriptResponse,
GetDatabaseInfoResponse,
DatabaseInfo,
CredentialInfo,
GetSourceDatabasesResponse,
GetSourceTablesRequestParams,
GetSourceTablesResponse,
GetSourceColumnDefinitionsRequestParams,
ColumnDefinition,
ExecutionResult,
DataSourceBrowsingParams,
SchemaViews,
DatabaseOverview
} from '../../services/contracts';
import { VDIManager } from './virtualizeDataInputManager';
// Stores important state and service methods used by the Virtualize Data wizard.
export class VirtualizeDataModel {
private _configInfoResponse: DataSourceWizardConfigInfoResponse;
private _databaseInfo: { [databaseName: string]: DatabaseInfo };
constructor(
private readonly _connection: azdata.connection.ConnectionProfile,
private readonly _wizardService: DataSourceWizardService,
private readonly _wizard: azdata.window.Wizard,
private readonly _vdiManager: VDIManager) {
this._databaseInfo = {};
}
public get connection(): azdata.connection.ConnectionProfile {
return this._connection;
}
public get wizardService(): DataSourceWizardService {
return this._wizardService;
}
public get wizard(): azdata.window.Wizard {
return this._wizard;
}
public get configInfoResponse(): DataSourceWizardConfigInfoResponse {
return this._configInfoResponse;
}
public get destDatabaseList(): DatabaseOverview[] {
return this._configInfoResponse ? (this._configInfoResponse.databaseList || []) : [];
}
public get sessionId(): string {
return this._configInfoResponse ? this._configInfoResponse.sessionId : undefined;
}
public get existingCredentials(): CredentialInfo[] {
let currentDbInfo = this._databaseInfo[this.selectedDestDatabaseName];
return currentDbInfo ? currentDbInfo.existingCredentials : undefined;
}
private get selectedDestDatabaseName(): string {
return this._vdiManager.destinationDatabaseName;
}
public get defaultSchema(): string {
let currentDbInfo = this._databaseInfo[this.selectedDestDatabaseName];
return currentDbInfo ? currentDbInfo.defaultSchema : undefined;
}
public get schemaList(): string[] {
let currentDbInfo = this._databaseInfo[this.selectedDestDatabaseName];
return currentDbInfo ? currentDbInfo.schemaList : [];
}
public async hasMasterKey(): Promise<boolean> {
let dbInfo = this._databaseInfo[this.selectedDestDatabaseName];
if (!dbInfo) {
await this.loadDatabaseInfo();
dbInfo = this._databaseInfo[this.selectedDestDatabaseName];
}
return dbInfo.hasMasterKey;
}
public showWizardError(title: string, description?: string): void {
this.showWizardMessage(title, description, azdata.window.MessageLevel.Error);
}
public showWizardInfo(title: string, description?: string): void {
this.showWizardMessage(title, description, azdata.window.MessageLevel.Information);
}
public showWizardWarning(title: string, description?: string): void {
this.showWizardMessage(title, description, azdata.window.MessageLevel.Warning);
}
public showWizardMessage(title: string, description: string, msgLevel: number): void {
this._wizard.message = {
text: title,
level: msgLevel,
description: description
};
}
public async createSession(): Promise<void> {
if (!this._configInfoResponse) {
try {
let credentials = await azdata.connection.getCredentials(this.connection.connectionId);
if (credentials) {
Object.assign(this.connection, credentials);
}
} catch (error) {
// swallow this as either it was integrated auth or we will fail later with login failed,
// which is a good error that makes sense to the user
}
try {
const timeout = vscode.workspace.getConfiguration('mssql').get('query.executionTimeout');
this.connection.options['QueryTimeout'] = timeout;
this._configInfoResponse = await this.wizardService.createDataSourceWizardSession(this.connection);
} catch (error) {
this.showWizardError(utils.getErrorMessage(error));
this._configInfoResponse = {
sessionId: undefined,
supportedSourceTypes: [],
databaseList: [],
serverMajorVersion: -1,
productLevel: undefined
};
}
}
}
public async validateInput(virtualizeDataInput: VirtualizeDataInput): Promise<boolean> {
try {
let response = await this._wizardService.validateVirtualizeDataInput(virtualizeDataInput);
if (!response.isValid) {
this.showWizardError(response.errorMessages.join('\n'));
}
return response.isValid;
} catch (error) {
this.showWizardError(utils.getErrorMessage(error));
return false;
}
}
public async getDatabaseInfo(databaseName: string): Promise<GetDatabaseInfoResponse> {
try {
let response = await this._wizardService.getDatabaseInfo({ sessionId: this.sessionId, databaseName: databaseName });
if (!response.isSuccess) {
this.showWizardError(response.errorMessages.join('\n'));
}
return response;
} catch (error) {
let eMessage = utils.getErrorMessage(error);
return { isSuccess: false, errorMessages: [eMessage], databaseInfo: undefined };
}
}
public async loadDatabaseInfo(databaseName?: string): Promise<DatabaseInfo> {
if (!databaseName) {
databaseName = this.selectedDestDatabaseName;
}
let databaseInfo: DatabaseInfo = this._databaseInfo[databaseName];
if (databaseInfo === undefined) {
let response = await this.getDatabaseInfo(databaseName);
if (response.isSuccess) {
databaseInfo = response.databaseInfo;
this._databaseInfo[databaseName] = databaseInfo;
} else {
this.showWizardError(response.errorMessages.join('\n'));
}
}
return databaseInfo;
}
public async generateScript(virtualizeDataInput: VirtualizeDataInput): Promise<GenerateScriptResponse> {
try {
let response = await this._wizardService.generateScript(virtualizeDataInput);
if (!response.isSuccess) {
this.showWizardError(response.errorMessages.join('\n'));
}
return response;
} catch (error) {
let eMessage = utils.getErrorMessage(error);
return { isSuccess: false, errorMessages: [eMessage], script: undefined };
}
}
public async processInput(virtualizeDataInput: VirtualizeDataInput): Promise<ProcessVirtualizeDataInputResponse> {
try {
let response = await this._wizardService.processVirtualizeDataInput(virtualizeDataInput);
if (!response.isSuccess) {
this.showWizardError(response.errorMessages.join('\n'));
}
return response;
} catch (error) {
let eMessage = utils.getErrorMessage(error);
return { isSuccess: false, errorMessages: [eMessage] };
}
}
public async getSourceDatabases(virtualizeDataInput: VirtualizeDataInput): Promise<GetSourceDatabasesResponse> {
try {
let response = await this._wizardService.getSourceDatabases(virtualizeDataInput);
if (!response.isSuccess) {
this.showWizardError(response.errorMessages.join('\n'));
}
return response;
} catch (error) {
let eMessage = utils.getErrorMessage(error);
this.showWizardError(eMessage);
return { isSuccess: false, errorMessages: [eMessage], databaseNames: undefined };
}
}
public async getSourceTables(requestParams: GetSourceTablesRequestParams): Promise<GetSourceTablesResponse> {
try {
let response = await this._wizardService.getSourceTables(requestParams);
if (!response.isSuccess) {
this.showWizardError(response.errorMessages.join('\n'));
}
return response;
} catch (error) {
let eMessage = utils.getErrorMessage(error);
this.showWizardError(eMessage);
return { isSuccess: false, errorMessages: [eMessage], schemaTablesList: undefined };
}
}
public async getSourceViewList(requestParams: DataSourceBrowsingParams<string>): Promise<ExecutionResult<SchemaViews[]>> {
let result: ExecutionResult<SchemaViews[]> = undefined;
try {
result = await this._wizardService.getSourceViewList(requestParams);
if (!result.isSuccess) {
this.showWizardError(result.errorMessages.join('\n'));
}
} catch (error) {
let eMessage = utils.getErrorMessage(error);
this.showWizardError(eMessage);
result = { isSuccess: false, errorMessages: [eMessage], returnValue: undefined };
}
return result;
}
public async getSourceColumnDefinitions(requestParams: GetSourceColumnDefinitionsRequestParams): Promise<ExecutionResult<ColumnDefinition[]>> {
let result: ExecutionResult<ColumnDefinition[]> = undefined;
try {
let response = await this._wizardService.getSourceColumnDefinitions(requestParams);
if (response && response.isSuccess) {
result = { isSuccess: true, errorMessages: undefined, returnValue: response.columnDefinitions };
} else {
result = { isSuccess: false, errorMessages: response.errorMessages, returnValue: undefined };
}
} catch (error) {
let eMessage = utils.getErrorMessage(error);
result = { isSuccess: false, errorMessages: [eMessage], returnValue: undefined };
}
return result;
}
}

View File

@@ -0,0 +1,364 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import * as azdata from 'azdata';
import { Uri, ThemeIcon } from 'vscode';
export enum TreeCheckboxState {
Intermediate = 0,
Checked = 1,
Unchecked = 2
}
export interface CheckboxTreeNodeArg {
treeId?: string;
nodeId?: string;
isRoot?: boolean;
label?: string;
maxLabelLength?: number;
isLeaf?: boolean;
isChecked?: boolean;
isEnabled?: boolean;
}
export abstract class CheckboxTreeNode implements azdata.TreeComponentItem {
protected _onNodeChange = new vscode.EventEmitter<void>();
protected _onTreeChange = new vscode.EventEmitter<CheckboxTreeNode>();
public readonly onNodeChange: vscode.Event<void> = this._onNodeChange.event;
public readonly onTreeChange: vscode.Event<CheckboxTreeNode> = this._onTreeChange.event;
private _nodeId: string;
public label: string;
private _isRoot: boolean;
private _isLeaf: boolean;
private _isChecked: boolean;
private _isEnabled: boolean;
private _treeId: string;
private _maxLabelLength: number;
private _rootNode: CheckboxTreeNode;
private _parent?: CheckboxTreeNode;
private _children: CheckboxTreeNode[];
private static _nodeRegistry: { [treeId: string]: Map<string, CheckboxTreeNode> } = {};
constructor(treeArg?: CheckboxTreeNodeArg) {
this._isRoot = false;
this._isLeaf = false;
this._isChecked = false;
this._isEnabled = true;
this.setArgs(treeArg);
}
public setArgs(treeArg: CheckboxTreeNodeArg): void {
if (treeArg) {
this._isRoot = treeArg.isRoot !== undefined ? treeArg.isRoot : this._isRoot;
this._treeId = treeArg.treeId || this._treeId;
this._nodeId = this._isRoot ? 'root' : (treeArg.nodeId || this._nodeId);
this.label = this._isRoot ? 'root' : (treeArg.label || this.label);
this._isLeaf = treeArg.isLeaf !== undefined ? treeArg.isLeaf : this._isLeaf;
this._isChecked = treeArg.isChecked !== undefined ? treeArg.isChecked : this._isChecked;
this._isEnabled = treeArg.isEnabled !== undefined ? treeArg.isEnabled : this._isEnabled;
this._maxLabelLength = treeArg.maxLabelLength || this._maxLabelLength;
}
CheckboxTreeNode.AddToNodeRegistry(this);
}
public static clearNodeRegistry(): void {
CheckboxTreeNode._nodeRegistry = {};
}
private static AddToNodeRegistry(node: CheckboxTreeNode): void {
if (node._treeId && node._nodeId) {
if (!CheckboxTreeNode._nodeRegistry[node._treeId]) {
CheckboxTreeNode._nodeRegistry[node._treeId] = new Map<string, CheckboxTreeNode>();
}
let registry = CheckboxTreeNode._nodeRegistry[node._treeId];
if (!registry.has(node._nodeId)) {
registry.set(node._nodeId, node);
} else {
throw new Error(`tree node with id: '${node._nodeId}' already exists`);
}
}
}
public static findNode(treeId: string, nodeId: string): CheckboxTreeNode {
let wantedNode: CheckboxTreeNode = undefined;
if (treeId && nodeId && CheckboxTreeNode._nodeRegistry[treeId] && CheckboxTreeNode._nodeRegistry[treeId].has(nodeId)) {
wantedNode = CheckboxTreeNode._nodeRegistry[treeId].get(nodeId);
}
return wantedNode;
}
public get id(): string {
return this._nodeId;
}
public get parent(): CheckboxTreeNode {
return this._parent;
}
public get children(): CheckboxTreeNode[] {
return this._children;
}
public set children(children: CheckboxTreeNode[]) {
if (children) {
this._children = children;
}
}
public get isRoot(): boolean {
return this._isRoot;
}
public get isLeaf(): boolean {
return this._isLeaf;
}
public set isLeaf(isLeaf: boolean) {
if (isLeaf !== undefined) {
this._isLeaf = isLeaf;
}
}
public get treeId(): string {
return this._treeId;
}
public set treeId(treeId: string) {
if (treeId) {
this._treeId = treeId;
}
}
public get checked(): boolean {
return this._isChecked;
}
public get enabled(): boolean {
return this._isEnabled;
}
public get hasChildren(): boolean {
return this._children !== undefined && this._children.length > 0;
}
protected get rootNode(): CheckboxTreeNode {
if (!this._rootNode && this._treeId) {
this._rootNode = CheckboxTreeNode._nodeRegistry[this._treeId].get('root');
}
return this._rootNode;
}
public get collapsibleState(): vscode.TreeItemCollapsibleState {
if (!this._isLeaf) {
return vscode.TreeItemCollapsibleState.Expanded;
} else {
vscode.TreeItemCollapsibleState.None;
}
}
public abstract get iconPath(): string | Uri | { light: string | Uri; dark: string | Uri } | ThemeIcon;
public get nodePath(): string {
return `${this.parent ? this.parent.nodePath + '-' : ''}${this.id}`;
}
public async setCheckedState(isChecked: boolean): Promise<void> {
let nodesToCheck: CheckboxTreeNode[] = [this];
while (nodesToCheck && nodesToCheck.length > 0) {
let node = nodesToCheck.shift();
if (node._isEnabled) {
node._isChecked = isChecked;
node.notifyStateChanged();
if (node.hasChildren) {
nodesToCheck = node._children.concat(nodesToCheck);
}
if (node.parent) {
await node.parent.refreshCheckedState();
}
}
}
this.notifyStateChanged();
}
public async refreshCheckedState(): Promise<void> {
let nodeToRefresh: CheckboxTreeNode = this;
while (nodeToRefresh && nodeToRefresh.hasChildren) {
if (nodeToRefresh._children.every(c => c.checked)) {
if (!nodeToRefresh._isChecked) {
nodeToRefresh._isChecked = true;
nodeToRefresh.notifyStateChanged();
nodeToRefresh = nodeToRefresh.parent;
} else {
nodeToRefresh = undefined;
}
} else if (nodeToRefresh._children.every(c => c.checked === false)) {
if (nodeToRefresh._isChecked !== false) {
nodeToRefresh._isChecked = false;
nodeToRefresh.notifyStateChanged();
nodeToRefresh = nodeToRefresh.parent;
} else {
nodeToRefresh = undefined;
}
} else {
if (nodeToRefresh._isChecked !== undefined) {
nodeToRefresh._isChecked = undefined;
nodeToRefresh.notifyStateChanged();
nodeToRefresh = nodeToRefresh.parent;
} else {
nodeToRefresh = undefined;
}
}
}
this.notifyStateChanged();
}
public async setEnable(isEnabled: boolean): Promise<void> {
if (isEnabled === undefined) {
isEnabled = true;
}
let nodesToSet: CheckboxTreeNode[] = [this];
while (nodesToSet && nodesToSet.length > 0) {
let node = nodesToSet.shift();
node._isEnabled = isEnabled;
node.notifyStateChanged();
if (node.hasChildren) {
nodesToSet = node._children.concat(nodesToSet);
}
if (node.parent) {
await node.parent.refreshEnableState();
}
}
this.notifyStateChanged();
}
public async refreshEnableState(): Promise<void> {
let nodeToRefresh: CheckboxTreeNode = this;
while (nodeToRefresh && nodeToRefresh.hasChildren) {
if (nodeToRefresh._children.every(c => c._isEnabled === false)) {
if (nodeToRefresh._isEnabled !== false) {
nodeToRefresh._isEnabled = false;
nodeToRefresh.notifyStateChanged();
nodeToRefresh = nodeToRefresh.parent;
} else {
nodeToRefresh = undefined;
}
} else {
if (!nodeToRefresh._isEnabled) {
nodeToRefresh._isEnabled = true;
nodeToRefresh.notifyStateChanged();
nodeToRefresh = nodeToRefresh.parent;
} else {
nodeToRefresh = undefined;
}
}
}
this.notifyStateChanged();
}
public notifyStateChanged(): void {
this._onNodeChange.fire();
let rootNode = this.rootNode;
if (rootNode) {
rootNode._onTreeChange.fire(this);
}
}
public get checkboxState(): TreeCheckboxState {
if (this.checked === undefined) {
return TreeCheckboxState.Intermediate;
} else {
return this.checked ? TreeCheckboxState.Checked : TreeCheckboxState.Unchecked;
}
}
public findNode(nodeId: string): CheckboxTreeNode {
let wantedNode: CheckboxTreeNode = undefined;
if (this.id === nodeId) {
wantedNode = this;
} else {
wantedNode = CheckboxTreeNode.findNode(this._treeId, nodeId);
}
return wantedNode;
}
public abstract getChildren(): Promise<CheckboxTreeNode[]>;
public clearChildren(): void {
if (this.children) {
this.children.forEach(child => {
child.clearChildren();
});
this._children = undefined;
this.notifyStateChanged();
}
}
public addChildNode(node: CheckboxTreeNode): void {
if (node) {
if (!this._children) {
this._children = [];
}
node._parent = this;
this._children.push(node);
}
}
}
export class CheckboxTreeDataProvider implements azdata.TreeComponentDataProvider<CheckboxTreeNode> {
private _onDidChangeTreeData = new vscode.EventEmitter<CheckboxTreeNode>();
constructor(private _root: CheckboxTreeNode) {
if (this._root) {
this._root.onTreeChange(node => {
this._onDidChangeTreeData.fire(node);
});
}
}
onDidChangeTreeData?: vscode.Event<CheckboxTreeNode | undefined | null> = this._onDidChangeTreeData.event;
/**
* Get [TreeItem](#TreeItem) representation of the `element`
*
* @param element The element for which [TreeItem](#TreeItem) representation is asked for.
* @return [TreeItem](#TreeItem) representation of the element
*/
getTreeItem(element: CheckboxTreeNode): azdata.TreeComponentItem | Thenable<azdata.TreeComponentItem> {
let item: azdata.TreeComponentItem = {};
item.label = element.label;
item.checked = element.checked;
item.collapsibleState = element.isLeaf ? vscode.TreeItemCollapsibleState.None : vscode.TreeItemCollapsibleState.Collapsed;
item.iconPath = element.iconPath;
item.enabled = element.enabled;
return item;
}
/**
* Get the children of `element` or root if no element is passed.
*
* @param element The element from which the provider gets children. Can be `undefined`.
* @return Children of `element` or root if no element is passed.
*/
getChildren(element?: CheckboxTreeNode): vscode.ProviderResult<CheckboxTreeNode[]> {
if (element) {
return element.getChildren();
} else {
return Promise.resolve(this._root.getChildren());
}
}
getParent(element?: CheckboxTreeNode): vscode.ProviderResult<CheckboxTreeNode> {
if (element) {
return Promise.resolve(element.parent);
} else {
return Promise.resolve(this._root);
}
}
}

View File

@@ -0,0 +1,187 @@
/*---------------------------------------------------------------------------------------------
* 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 nls from 'vscode-nls';
const localize = nls.loadMessageBundle();
import { ApiWrapper } from '../../apiWrapper';
import { DataSourceWizardService } from '../../services/contracts';
import { SelectDataSourcePage } from './selectDataSourcePage';
import { ConnectionDetailsPage } from './connectionDetailsPage';
import { SummaryPage } from './summaryPage';
import { ObjectMappingPage } from './objectMappingPage';
import { IWizardPageWrapper } from '../wizardPageWrapper';
import { VirtualizeDataModel } from './virtualizeDataModel';
import { sqlFileExtension } from '../../constants';
import { AppContext } from '../../appContext';
import { CreateMasterKeyPage } from './createMasterKeyPage';
import { getErrorMessage } from '../../utils';
import { VDIManager } from './virtualizeDataInputManager';
export class VirtualizeDataWizard {
private _wizard: azdata.window.Wizard;
private _wizardPageWrappers: IWizardPageWrapper[];
private _dataModel: VirtualizeDataModel;
private _vdiManager: VDIManager;
constructor(
private _connection: azdata.connection.ConnectionProfile,
private _wizardService: DataSourceWizardService,
private _appContext: AppContext) {
}
public async openWizard(): Promise<void> {
await this.initialize();
await this._wizard.open();
}
private async initialize(): Promise<void> {
this._wizard = azdata.window.createWizard(localize('getExtDataTitle', 'Virtualize Data'));
this._wizard.nextButton.enabled = false;
// TODO: Add placeholder loading page or spinner here
this._vdiManager = new VDIManager();
this._dataModel = new VirtualizeDataModel(this._connection, this._wizardService, this._wizard, this._vdiManager);
await this._dataModel.createSession();
this._wizardPageWrappers = [
new SelectDataSourcePage(this),
new CreateMasterKeyPage(this._dataModel, this._vdiManager, this.appContext),
new ConnectionDetailsPage(this._dataModel, this._vdiManager, this._appContext),
new ObjectMappingPage(this._dataModel, this._vdiManager, this._appContext),
new SummaryPage(this._dataModel, this._vdiManager, this._appContext)
];
this._wizardPageWrappers.forEach(w => {
let page = w.getPage();
if (page) { page['owner'] = w; }
});
this._vdiManager.setInputPages(this._wizardPageWrappers);
this._vdiManager.setVirtualizeDataModel(this._dataModel);
this._wizard.pages = this._wizardPageWrappers.map(wrapper => wrapper.getPage());
this._wizard.displayPageTitles = true;
this._wizard.cancelButton.onClick(() => this.actionClose());
this._wizard.doneButton.label = localize('doneButtonLabel', 'Create');
this._wizard.doneButton.hidden = true;
this._wizard.generateScriptButton.onClick(async () => await this.actionGenerateScript());
this._wizard.generateScriptButton.hidden = true;
this._wizard.generateScriptButton.enabled = false;
this._wizard.registerNavigationValidator(async (info) => await this.actionValidateInputAndUpdateNextPage(info));
this._wizard.onPageChanged(info => this.actionChangePage(info));
}
private async actionClose(): Promise<void> {
try {
let sessionId = this._dataModel.sessionId;
if (sessionId) {
await this._wizardService.disposeWizardSession(sessionId);
}
} catch (error) {
this.apiWrapper.showErrorMessage(error.toString());
}
}
private async actionGenerateScript(): Promise<void> {
try {
// Disable the button while generating the script to prevent an issue where multiple quick
// button presses would duplicate the script. (There's no good reason to allow multiple
// scripts to be generated anyways)
this._wizard.generateScriptButton.enabled = false;
let virtualizeDataInput = this._vdiManager.virtualizeDataInput;
let response = await this._dataModel.generateScript(virtualizeDataInput);
if (response.isSuccess) {
let sqlScript: string = response.script;
let doc = await this.apiWrapper.openTextDocument({ language: sqlFileExtension, content: sqlScript });
await this.apiWrapper.showDocument(doc);
await azdata.queryeditor.connect(doc.uri.toString(), this._dataModel.connection.connectionId);
this._dataModel.showWizardInfo(
localize('openScriptMsg',
'The script has opened in a document window. You can view it once the wizard is closed.'));
} else {
let eMessage = response.errorMessages.join('\n');
this._dataModel.showWizardError(eMessage);
}
} catch (error) {
this._dataModel.showWizardError(error.toString());
// re-enable button if an error occurred since we didn't actually generate a script
this._wizard.generateScriptButton.enabled = true;
}
}
private actionChangePage(info: azdata.window.WizardPageChangeInfo): void {
this.toggleLastPageButtons(info.newPage === (this._wizard.pages.length - 1));
}
private toggleLastPageButtons(isLastPage: boolean): void {
this._wizard.doneButton.hidden = !isLastPage;
this._wizard.generateScriptButton.hidden = !isLastPage;
this._wizard.generateScriptButton.enabled = isLastPage;
}
private async actionValidateInputAndUpdateNextPage(info: azdata.window.WizardPageChangeInfo): Promise<boolean> {
this._wizard.message = undefined;
// Skip validation for moving to a previous page
if (info.newPage < info.lastPage) {
return true;
}
try {
let currentPageWrapper: IWizardPageWrapper = this.GetWizardPageWrapper(info.lastPage);
if (!currentPageWrapper || !(await currentPageWrapper.validate())) { return false; }
if (!info.newPage) { return true; }
let newPageWrapper: IWizardPageWrapper = this.GetWizardPageWrapper(info.newPage);
if (!newPageWrapper) { return false; }
await newPageWrapper.updatePage();
return true;
} catch (error) {
this._dataModel.showWizardError(getErrorMessage(error));
}
return false;
}
private GetWizardPageWrapper(pageIndex: number): IWizardPageWrapper {
if (!this._wizard || !this._wizard.pages || this._wizard.pages.length === 0
|| pageIndex < 0 || pageIndex >= this._wizard.pages.length) { return undefined; }
let wizardPage = this._wizard.pages[pageIndex];
return wizardPage && wizardPage['owner'];
}
private get apiWrapper(): ApiWrapper {
return this._appContext.apiWrapper;
}
public get appContext(): AppContext {
return this._appContext;
}
public get dataModel(): VirtualizeDataModel {
return this._dataModel;
}
public get vdiManager(): VDIManager {
return this._vdiManager;
}
public get wizard(): azdata.window.Wizard {
return this._wizard;
}
public get wizardPageWrappers(): IWizardPageWrapper[] {
return this._wizardPageWrappers;
}
}

View File

@@ -0,0 +1,119 @@
/*---------------------------------------------------------------------------------------------
* 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 nls from 'vscode-nls';
const localize = nls.loadMessageBundle();
import { ICommandViewContext, Command, ICommandObjectExplorerContext, ICommandUnknownContext } from '../command';
import { VirtualizeDataWizard } from './virtualizeData/virtualizeDataWizard';
import { DataSourceWizardService } from '../services/contracts';
import { AppContext } from '../appContext';
import { getErrorMessage } from '../utils';
import * as constants from '../constants';
import { TableFromFileWizard } from './tableFromFile/tableFromFileWizard';
import { getNodeFromMssqlProvider } from '../hdfsCommands';
import { HdfsFileSourceNode } from '../hdfsProvider';
export class OpenVirtualizeDataWizardCommand extends Command {
private readonly dataWizardTask: OpenVirtualizeDataWizardTask;
constructor(appContext: AppContext, wizardService: DataSourceWizardService) {
super(constants.virtualizeDataCommand, appContext);
this.dataWizardTask = new OpenVirtualizeDataWizardTask(appContext, wizardService);
}
protected async preExecute(context: ICommandUnknownContext | ICommandObjectExplorerContext, args: object = {}): Promise<any> {
return this.execute(context, args);
}
async execute(context: ICommandUnknownContext | ICommandObjectExplorerContext, ...args: any[]): Promise<void> {
let profile: azdata.IConnectionProfile = undefined;
if (context && context.type === constants.ObjectExplorerService && context.explorerContext) {
profile = context.explorerContext.connectionProfile;
}
this.dataWizardTask.execute(profile, args);
}
}
export class OpenVirtualizeDataWizardTask {
constructor(private appContext: AppContext, private wizardService: DataSourceWizardService) {
}
async execute(profile: azdata.IConnectionProfile, ...args: any[]): Promise<void> {
try {
let connection: azdata.connection.ConnectionProfile;
if (profile) {
connection = convertIConnectionProfile(profile);
} else {
connection = await azdata.connection.getCurrentConnection();
if (!connection) {
this.appContext.apiWrapper.showErrorMessage(localize('noConnection', 'Data Virtualization requires a connection to be selected.'));
return;
}
}
let wizard = new VirtualizeDataWizard(connection, this.wizardService, this.appContext);
await wizard.openWizard();
} catch (error) {
this.appContext.apiWrapper.showErrorMessage(getErrorMessage(error));
}
}
}
export class OpenMssqlHdfsTableFromFileWizardCommand extends Command {
constructor(appContext: AppContext, private wizardService: DataSourceWizardService) {
super(constants.mssqlHdfsTableFromFileCommand, appContext);
}
protected async preExecute(context: ICommandViewContext | ICommandObjectExplorerContext, args: object = {}): Promise<any> {
return this.execute(context, args);
}
async execute(context: ICommandViewContext | ICommandObjectExplorerContext, ...args: any[]): Promise<void> {
try {
let connection: azdata.connection.ConnectionProfile;
if (context && context.type === constants.ObjectExplorerService && context.explorerContext) {
connection = convertIConnectionProfile(context.explorerContext.connectionProfile);
}
if (!connection) {
connection = await azdata.connection.getCurrentConnection();
if (!connection) {
this.appContext.apiWrapper.showErrorMessage(localize('noConnection', 'Data Virtualization requires a connection to be selected.'));
return;
}
}
let fileNode = await getNodeFromMssqlProvider<HdfsFileSourceNode>(context, this.appContext);
let wizard = new TableFromFileWizard(connection, this.appContext, this.wizardService);
await wizard.start(fileNode);
} catch (error) {
this.appContext.apiWrapper.showErrorMessage(getErrorMessage(error));
}
}
}
function convertIConnectionProfile(profile: azdata.IConnectionProfile): azdata.connection.ConnectionProfile {
let connection: azdata.connection.ConnectionProfile;
if (profile) {
connection = {
providerId: profile.providerName,
connectionId: profile.id,
connectionName: profile.connectionName,
serverName: profile.serverName,
databaseName: profile.databaseName,
userName: profile.userName,
password: profile.password,
authenticationType: profile.authenticationType,
savePassword: profile.savePassword,
groupFullName: profile.groupFullName,
groupId: profile.groupId,
saveProfile: profile.saveProfile,
azureTenantId: profile.azureTenantId,
options: {}
};
}
return connection;
}

View File

@@ -0,0 +1,23 @@
/*---------------------------------------------------------------------------------------------
* 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 { VirtualizeDataInput } from '../services/contracts';
export interface IWizardPageWrapper {
// Returns underlying wizard page object.
getPage(): azdata.window.WizardPage;
// Called for the current page after clicking the Wizard's Next button.
// Returns boolean indicating whether validation was successful and thus
// if page can be changed.
validate(): Promise<boolean>;
// Updates the wizard page by retrieving current info from the backing data model.
updatePage(): Promise<void>;
// Adds this page's input contributions to the provided data input object
getInputValues(existingInput: VirtualizeDataInput): void;
}