mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-01-27 01:25:36 -05:00
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:
@@ -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>;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user