Adding Derived Columns to ADS Flatfile Import (#16795)

* Adding derived column boilerplate

* brandan preliminary frontend changes

* empty commit

* added new param

* updating contracts, dialogue changes

* utils changes, saving timeout attempt

* pushing for aasim

* Cleaning up code and fixing the issue in theory

* changing button, did not solve independent scroll

* Fixing the scroll bar issue

* updating flat file service

* adding override keyword to overrriden method

* improving UI

* pushing changes associated with resolved comments

* localizing strings, editing comments

* all comments resolved

* Fixing a test

* updating import package
Updating azure MFA bug

* Clearing navigation validator
Fixing broken table name change

* fixed prose test

* removing unused code from tests

* Fixed PR comments

* Fixing some PR comments

* WIP

* Fixing transformation code and create derived column dialog styling

* removing unused code

* Adding comment for console log

* fixed table styling

* Adding some aria labels

* Fixed some code cleanup issues

* update import service

Co-authored-by: Aasim Khan <aasimkhan30@gmail.com>
Co-authored-by: bnhoule <t-bhoule@microsoft.com>
This commit is contained in:
bnhoule
2021-09-21 17:11:00 -05:00
committed by GitHub
parent fad2963202
commit d3e163a1d7
15 changed files with 569 additions and 108 deletions

View File

@@ -91,7 +91,7 @@ export abstract class BasePage {
return values;
}
public async getDatabaseValues(): Promise<{ displayName: string, name: string }[]> {
public async getDatabaseValues(): Promise<azdata.CategoryValue[]> {
let idx = -1;
let count = -1;
let values = (await azdata.connection.listDatabases(this.model.server.connectionId)).map(db => {

View File

@@ -19,6 +19,8 @@ export interface ImportDataModel {
schema: string;
filePath: string;
fileType: string;
originalProseColumns: ColumnMetadata[];
newFileSelected: boolean;
}
/**

View File

@@ -23,6 +23,7 @@ export class FlatFileWizard {
public page3: azdata.window.WizardPage;
public page4: azdata.window.WizardPage;
public createDerivedColumnButton: azdata.window.Button;
private importAnotherFileButton: azdata.window.Button;
constructor(
@@ -89,6 +90,8 @@ export class FlatFileWizard {
await summaryPage.start();
});
this.createDerivedColumnButton = azdata.window.createButton(constants.createDerivedColumn);
this.createDerivedColumnButton.hidden = true;
this.importAnotherFileButton = azdata.window.createButton(constants.importNewFileText);
this.importAnotherFileButton.onClick(() => {
@@ -99,7 +102,7 @@ export class FlatFileWizard {
});
this.importAnotherFileButton.hidden = true;
this.wizard.customButtons = [this.importAnotherFileButton];
this.wizard.customButtons = [this.importAnotherFileButton, this.createDerivedColumnButton];
this.wizard.onPageChanged(async (event) => {
let newPageIdx = event.newPage;
let lastPageIdx = event.lastPage;

View File

@@ -7,7 +7,7 @@ import * as azdata from 'azdata';
import * as vscode from 'vscode';
import { ImportPage } from '../api/importPage';
import * as constants from '../../common/constants';
import * as fs from 'fs';
import { promises as fs } from 'fs';
export class FileConfigPage extends ImportPage {
@@ -85,7 +85,6 @@ export class FileConfigPage extends ImportPage {
this._schemaLoader = schemaLoader;
}
private tableNames: string[] = [];
async start(): Promise<boolean> {
@@ -144,6 +143,7 @@ export class FileConfigPage extends ImportPage {
this.model.server = connectionValue.connection;
await this.populateDatabaseDropdown();
}
await this.populateDatabaseDropdown();
});
return {
@@ -198,33 +198,35 @@ export class FileConfigPage extends ImportPage {
this.databaseDropdown.values = [];
this.schemaDropdown.values = [];
if (!this.model.server) {
//TODO handle error case
this.databaseDropdown.loading = false;
return false;
}
let defaultServerDatabase = this.model.server.options.database;
let values: any[];
try {
values = await this.getDatabaseValues();
} catch (error) {
// This code is used in case of contained databases when the query will return an error.
console.log(error);
values = [{ displayName: defaultServerDatabase, name: defaultServerDatabase }];
this.databaseDropdown.editable = false;
if (!this.model.server) {
//TODO handle error case
this.databaseDropdown.loading = false;
return false;
}
let defaultServerDatabase = this.model.server.options.database;
let values: azdata.CategoryValue[];
try {
values = await this.getDatabaseValues();
} catch (error) {
// This code is used in case of contained databases when the query will return an error.
console.log(error);
values = [{ displayName: defaultServerDatabase, name: defaultServerDatabase }];
this.databaseDropdown.editable = false;
}
this.model.database = defaultServerDatabase;
this.databaseDropdown.updateProperties({
values: values
});
this.databaseDropdown.value = { displayName: this.model.database, name: this.model.database };
} finally {
this.databaseDropdown.loading = false;
}
this.model.database = defaultServerDatabase;
this.databaseDropdown.updateProperties({
values: values
});
this.databaseDropdown.value = { displayName: this.model.database, name: this.model.database };
this.databaseDropdown.loading = false;
return true;
}
@@ -232,16 +234,29 @@ export class FileConfigPage extends ImportPage {
this.fileTextBox = this.view.modelBuilder.inputBox().withProps({
required: true,
validationErrorMessage: constants.invalidFileLocationError
}).withValidation((component) => {
return fs.existsSync(component.value);
}).withValidation(async (component) => {
if (component.value) {
try {
await fs.access(component.value);
return true;
} catch (e) {
return false;
}
}
return false;
}).component();
this.fileTextBox.onTextChanged(e => {
this.model.newFileSelected = true;
});
this.fileButton = this.view.modelBuilder.button().withProps({
label: constants.browseFilesText,
secondary: true
}).component();
this.fileButton.onDidClick(async (click) => {
this.model.newFileSelected = true;
let fileUris = await vscode.window.showOpenDialog(
{
canSelectFiles: true,
@@ -324,6 +339,7 @@ export class FileConfigPage extends ImportPage {
}).component();
this.tableNameTextBox.onTextChanged((tableName) => {
this.model.newFileSelected = true;
this.model.table = tableName;
});
@@ -415,33 +431,6 @@ export class FileConfigPage extends ImportPage {
delete this.model.database;
delete this.model.schema;
}
// private async populateTableNames(): Promise<boolean> {
// this.tableNames = [];
// let databaseName = (<azdata.CategoryValue>this.databaseDropdown.value).name;
//
// if (!databaseName || databaseName.length === 0) {
// this.tableNames = [];
// return false;
// }
//
// let connectionUri = await azdata.connection.getUriForConnection(this.model.server.connectionId);
// let queryProvider = azdata.dataprotocol.getProvider<azdata.QueryProvider>(this.model.server.providerName, azdata.DataProviderType.QueryProvider);
// let results: azdata.SimpleExecuteResult;
//
// try {
// //let query = sqlstring.format('USE ?; SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = \'BASE TABLE\'', [databaseName]);
// //results = await queryProvider.runQueryAndReturn(connectionUri, query);
// } catch (e) {
// return false;
// }
//
// this.tableNames = results.rows.map(row => {
// return row[0].displayValue;
// });
//
// return true;
// }
}

View File

@@ -104,7 +104,6 @@ export class ModifyColumnsPage extends ImportPage {
});
});
this.form = this.view.modelBuilder.formContainer()
.withFormItems(
[
@@ -131,15 +130,28 @@ export class ModifyColumnsPage extends ImportPage {
await this.populateTable();
this.instance.changeNextButtonLabel(constants.importDataText);
this.loading.loading = false;
this.instance.registerNavigationValidator((info) => {
return this.table.data && this.table.data.length > 0;
});
return true;
}
override async onPageLeave(): Promise<boolean> {
await this.emptyTable();
this.instance.changeNextButtonLabel(constants.nextText);
this.instance.registerNavigationValidator((info) => {
return true;
});
return undefined;
}
private emptyTable() {
this.table.updateProperties({
data: [],
columns: []
});
}
override async cleanup(): Promise<boolean> {
delete this.model.proseColumns;
this.instance.changeNextButtonLabel(constants.nextText);
@@ -147,12 +159,6 @@ export class ModifyColumnsPage extends ImportPage {
return true;
}
public override setupNavigationValidator() {
this.instance.registerNavigationValidator((info) => {
return this.table.data && this.table.data.length > 0;
});
}
private async populateTable() {
let data: ColumnMetadataArray[] = [];

View File

@@ -2,17 +2,17 @@
* 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 { ImportPage } from '../api/importPage';
import * as constants from '../../common/constants';
import { DerivedColumnDialog } from '../../dialogs/derivedColumnDialog';
import * as vscode from 'vscode';
export class ProsePreviewPage extends ImportPage {
private _table: azdata.TableComponent;
private _loading: azdata.LoadingComponent;
private _form: azdata.FormContainer;
private _refresh: azdata.ButtonComponent;
private _resultTextComponent: azdata.TextComponent;
private _isSuccess: boolean;
@@ -40,14 +40,6 @@ export class ProsePreviewPage extends ImportPage {
this._form = form;
}
public get refresh(): azdata.ButtonComponent {
return this._refresh;
}
public set refresh(refresh: azdata.ButtonComponent) {
this._refresh = refresh;
}
public get resultTextComponent(): azdata.TextComponent {
return this._resultTextComponent;
}
@@ -70,14 +62,22 @@ export class ProsePreviewPage extends ImportPage {
columns: undefined,
forceFitColumns: azdata.ColumnSizingMode.DataFit
}).component();
this.refresh = this.view.modelBuilder.button().withProps({
label: constants.refreshText,
isFile: false,
secondary: true
}).component();
this.refresh.onDidClick(async () => {
await this.onPageEnter();
this.instance.createDerivedColumnButton.onClick(async (e) => {
const derivedColumnDialog = new DerivedColumnDialog(this.model, this.provider);
const response = await derivedColumnDialog.createDerivedColumn();
if (response) {
this.model.proseColumns.push({
columnName: response.derivedColumnName,
dataType: 'nvarchar(MAX)',
primaryKey: false,
nullable: true
});
response.derivedColumnDataPreview.forEach((v, i) => {
this.model.proseDataPreview[i].push(v);
});
this.populateTable(this.model.proseDataPreview, this.model.proseColumns.map(c => c.columnName));
}
});
this.loading = this.view.modelBuilder.loadingComponent().component();
@@ -94,9 +94,9 @@ export class ProsePreviewPage extends ImportPage {
},
{
component: this.table,
title: '',
actions: [this.refresh]
title: ''
}
]).component();
this.loading.component = this.form;
@@ -107,22 +107,30 @@ export class ProsePreviewPage extends ImportPage {
}
async onPageEnter(): Promise<boolean> {
this.loading.loading = true;
let proseResult: boolean;
let error: string;
try {
proseResult = await this.handleProse();
} catch (ex) {
error = ex.toString();
const enablePreviewFeatures = vscode.workspace.getConfiguration('workbench').get('enablePreviewFeatures');
if (this.model.newFileSelected) {
this.loading.loading = true;
try {
proseResult = await this.learnFile();
} catch (ex) {
error = ex.toString();
this.instance.wizard.message = {
level: azdata.window.MessageLevel.Error,
text: error
};
}
this.model.newFileSelected = false;
this.loading.loading = false;
}
this.loading.loading = false;
if (proseResult) {
if (!this.model.newFileSelected || proseResult) {
await this.populateTable(this.model.proseDataPreview, this.model.proseColumns.map(c => c.columnName));
this.isSuccess = true;
if (this.form) {
this.resultTextComponent.value = constants.successTitleText;
}
this.instance.createDerivedColumnButton.hidden = !enablePreviewFeatures;
return true;
} else {
await this.populateTable([], []);
@@ -135,7 +143,7 @@ export class ProsePreviewPage extends ImportPage {
}
override async onPageLeave(): Promise<boolean> {
await this.emptyTable();
this.instance.createDerivedColumnButton.hidden = true;
return true;
}
@@ -156,7 +164,7 @@ export class ProsePreviewPage extends ImportPage {
});
}
private async handleProse(): Promise<boolean> {
private async learnFile(): Promise<boolean> {
const response = await this.provider.sendPROSEDiscoveryRequest({
filePath: this.model.filePath,
tableName: this.model.table,
@@ -170,6 +178,7 @@ export class ProsePreviewPage extends ImportPage {
}
this.model.proseColumns = [];
this.model.originalProseColumns = [];
if (response.columnInfo) {
response.columnInfo.forEach((column) => {
this.model.proseColumns.push({
@@ -178,6 +187,12 @@ export class ProsePreviewPage extends ImportPage {
primaryKey: false,
nullable: column.isNullable
});
this.model.originalProseColumns.push({
columnName: column.name,
dataType: column.sqlType,
primaryKey: false,
nullable: column.isNullable
});
});
return true;
}
@@ -203,8 +218,4 @@ export class ProsePreviewPage extends ImportPage {
width: '700',
});
}
private async emptyTable() {
this.table.updateProperties([]);
}
}

View File

@@ -50,7 +50,12 @@ export class SummaryPage extends ImportPage {
async start(): Promise<boolean> {
this.table = this.view.modelBuilder.table().component();
this.statusText = this.view.modelBuilder.text().component();
this.statusText = this.view.modelBuilder.text().withProps({
CSSStyles: {
'user-select': 'text',
'font-size': '13px'
}
}).component();
this.loading = this.view.modelBuilder.loadingComponent().withItem(this.statusText).component();
this.form = this.view.modelBuilder.formContainer().withFormItems(