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,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;
}