mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-01-14 01:25:37 -05:00
* Rebase from main branch * import from module * Add mssql module ref Co-authored-by: Charles Gagnon <chgagnon@microsoft.com>
1017 lines
37 KiB
TypeScript
1017 lines
37 KiB
TypeScript
/*---------------------------------------------------------------------------------------------
|
|
* 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 path from 'path';
|
|
import * as sqldbproj from 'sqldbproj';
|
|
import * as mssql from 'mssql';
|
|
import * as loc from '../localizedConstants';
|
|
import { SchemaCompareMainWindow } from '../schemaCompareMainWindow';
|
|
import { TelemetryReporter, TelemetryViews } from '../telemetry';
|
|
import { getEndpointName, getRootPath, exists, getAzdataApi, getSchemaCompareEndpointString } from '../utils';
|
|
|
|
const titleFontSize: number = 13;
|
|
|
|
interface Deferred<T> {
|
|
resolve: (result: T | Promise<T>) => void;
|
|
reject: (reason: any) => void;
|
|
}
|
|
|
|
export class SchemaCompareDialog {
|
|
public dialog: azdata.window.Dialog;
|
|
public dialogName: string;
|
|
private schemaCompareTab: azdata.window.DialogTab;
|
|
private sourceDacpacRadioButton: azdata.RadioButtonComponent;
|
|
private sourceDatabaseRadioButton: azdata.RadioButtonComponent;
|
|
private sourceProjectRadioButton: azdata.RadioButtonComponent;
|
|
private sourceDacpacComponent: azdata.FormComponent;
|
|
private sourceProjectFilePathComponent: azdata.FormComponent;
|
|
private sourceTextBox: azdata.InputBoxComponent;
|
|
private sourceFileButton: azdata.ButtonComponent;
|
|
private sourceServerComponent: azdata.FormComponent;
|
|
protected sourceServerDropdown: azdata.DropDownComponent;
|
|
private sourceConnectionButton: azdata.ButtonComponent;
|
|
private sourceDatabaseComponent: azdata.FormComponent;
|
|
private sourceDatabaseDropdown: azdata.DropDownComponent;
|
|
private sourceEndpointType: mssql.SchemaCompareEndpointType;
|
|
private sourceDbEditable: string;
|
|
private sourceDacpacPath: string;
|
|
private sourceProjectFilePath: string;
|
|
private targetDacpacComponent: azdata.FormComponent;
|
|
private targetProjectFilePathComponent: azdata.FormComponent;
|
|
private targetProjectStructureComponent: azdata.FormComponent;
|
|
private targetTextBox: azdata.InputBoxComponent;
|
|
private targetFileButton: azdata.ButtonComponent;
|
|
private targetStructureDropdown: azdata.DropDownComponent;
|
|
private targetServerComponent: azdata.FormComponent;
|
|
protected targetServerDropdown: azdata.DropDownComponent;
|
|
private targetConnectionButton: azdata.ButtonComponent;
|
|
private targetDatabaseComponent: azdata.FormComponent;
|
|
private targetDatabaseDropdown: azdata.DropDownComponent;
|
|
private targetDacpacPath: string;
|
|
private targetProjectFilePath: string;
|
|
private targetEndpointType: mssql.SchemaCompareEndpointType;
|
|
private targetDbEditable: string;
|
|
private previousSource: mssql.SchemaCompareEndpointInfo;
|
|
private previousTarget: mssql.SchemaCompareEndpointInfo;
|
|
private formBuilder: azdata.FormBuilder;
|
|
private connectionId: string;
|
|
private toDispose: vscode.Disposable[] = [];
|
|
private initDialogComplete: Deferred<void>;
|
|
private initDialogPromise: Promise<void> = new Promise<void>((resolve, reject) => this.initDialogComplete = { resolve, reject });
|
|
|
|
private textBoxWidth: number = 280;
|
|
|
|
public promise;
|
|
public promise2;
|
|
|
|
constructor(private schemaCompareMainWindow: SchemaCompareMainWindow, private view?: azdata.ModelView, private extensionContext?: vscode.ExtensionContext) {
|
|
this.previousSource = schemaCompareMainWindow.sourceEndpointInfo;
|
|
this.previousTarget = schemaCompareMainWindow.targetEndpointInfo;
|
|
|
|
this.dialog = azdata.window.createModelViewDialog(loc.SchemaCompareLabel);
|
|
this.dialog.registerCloseValidator(async () => {
|
|
return this.validate();
|
|
});
|
|
}
|
|
|
|
protected async initializeDialog(): Promise<void> {
|
|
this.schemaCompareTab = azdata.window.createTab(loc.SchemaCompareLabel);
|
|
await this.initializeSchemaCompareTab();
|
|
this.dialog.content = [this.schemaCompareTab];
|
|
}
|
|
|
|
public async openDialog(): Promise<void> {
|
|
// connection to use if schema compare wasn't launched from a database or no previous source/target
|
|
let connection = await azdata.connection.getCurrentConnection();
|
|
if (connection) {
|
|
this.connectionId = connection.connectionId;
|
|
}
|
|
|
|
this.dialog = azdata.window.createModelViewDialog(loc.SchemaCompareLabel);
|
|
await this.initializeDialog();
|
|
|
|
this.dialog.okButton.label = loc.OkButtonText;
|
|
this.dialog.okButton.enabled = false;
|
|
this.toDispose.push(this.dialog.okButton.onClick(async () => await this.handleOkButtonClick()));
|
|
|
|
this.dialog.cancelButton.label = loc.CancelButtonText;
|
|
this.toDispose.push(this.dialog.cancelButton.onClick(async () => await this.cancel()));
|
|
|
|
azdata.window.openDialog(this.dialog);
|
|
await this.initDialogPromise;
|
|
}
|
|
|
|
public async execute(): Promise<void> {
|
|
if (this.sourceEndpointType === mssql.SchemaCompareEndpointType.Database) {
|
|
const sourceServerDropdownValue = this.sourceServerDropdown.value as ConnectionDropdownValue;
|
|
const ownerUri = await azdata.connection.getUriForConnection(sourceServerDropdownValue.connection.connectionId);
|
|
|
|
this.schemaCompareMainWindow.sourceEndpointInfo = {
|
|
endpointType: mssql.SchemaCompareEndpointType.Database,
|
|
serverDisplayName: sourceServerDropdownValue.displayName,
|
|
serverName: sourceServerDropdownValue.name,
|
|
databaseName: this.sourceDatabaseDropdown.value.toString(),
|
|
ownerUri: ownerUri,
|
|
projectFilePath: '',
|
|
targetScripts: [],
|
|
folderStructure: '',
|
|
packageFilePath: '',
|
|
dataSchemaProvider: '',
|
|
connectionDetails: undefined,
|
|
connectionName: sourceServerDropdownValue.connection.options.connectionName
|
|
};
|
|
} else if (this.sourceEndpointType === mssql.SchemaCompareEndpointType.Dacpac) {
|
|
this.schemaCompareMainWindow.sourceEndpointInfo = {
|
|
endpointType: mssql.SchemaCompareEndpointType.Dacpac,
|
|
serverDisplayName: '',
|
|
serverName: '',
|
|
databaseName: '',
|
|
ownerUri: '',
|
|
projectFilePath: '',
|
|
targetScripts: [],
|
|
folderStructure: '',
|
|
dataSchemaProvider: '',
|
|
packageFilePath: this.sourceTextBox.value,
|
|
connectionDetails: undefined
|
|
};
|
|
} else {
|
|
this.schemaCompareMainWindow.sourceEndpointInfo = {
|
|
endpointType: mssql.SchemaCompareEndpointType.Project,
|
|
projectFilePath: this.sourceTextBox.value,
|
|
targetScripts: await this.getProjectScriptFiles(this.sourceTextBox.value),
|
|
dataSchemaProvider: await this.getDatabaseSchemaProvider(this.sourceTextBox.value),
|
|
folderStructure: '',
|
|
serverDisplayName: '',
|
|
serverName: '',
|
|
databaseName: '',
|
|
ownerUri: '',
|
|
packageFilePath: '',
|
|
connectionDetails: undefined
|
|
};
|
|
}
|
|
|
|
if (this.targetEndpointType === mssql.SchemaCompareEndpointType.Database) {
|
|
const targetServerDropdownValue = this.targetServerDropdown.value as ConnectionDropdownValue;
|
|
const ownerUri = await azdata.connection.getUriForConnection(targetServerDropdownValue.connection.connectionId);
|
|
|
|
this.schemaCompareMainWindow.targetEndpointInfo = {
|
|
endpointType: mssql.SchemaCompareEndpointType.Database,
|
|
serverDisplayName: targetServerDropdownValue.displayName,
|
|
serverName: targetServerDropdownValue.name,
|
|
databaseName: this.targetDatabaseDropdown.value.toString(),
|
|
ownerUri: ownerUri,
|
|
projectFilePath: '',
|
|
folderStructure: '',
|
|
targetScripts: [],
|
|
packageFilePath: '',
|
|
dataSchemaProvider: '',
|
|
connectionDetails: undefined,
|
|
connectionName: targetServerDropdownValue.connection.options.connectionName
|
|
};
|
|
} else if (this.targetEndpointType === mssql.SchemaCompareEndpointType.Dacpac) {
|
|
this.schemaCompareMainWindow.targetEndpointInfo = {
|
|
endpointType: mssql.SchemaCompareEndpointType.Dacpac,
|
|
serverDisplayName: '',
|
|
serverName: '',
|
|
databaseName: '',
|
|
ownerUri: '',
|
|
projectFilePath: '',
|
|
folderStructure: '',
|
|
targetScripts: [],
|
|
dataSchemaProvider: '',
|
|
packageFilePath: this.targetTextBox.value,
|
|
connectionDetails: undefined
|
|
};
|
|
} else {
|
|
this.schemaCompareMainWindow.targetEndpointInfo = {
|
|
endpointType: mssql.SchemaCompareEndpointType.Project,
|
|
projectFilePath: this.targetTextBox.value,
|
|
folderStructure: this.targetStructureDropdown!.value as string,
|
|
targetScripts: await this.getProjectScriptFiles(this.targetTextBox.value),
|
|
dataSchemaProvider: await this.getDatabaseSchemaProvider(this.targetTextBox.value),
|
|
serverDisplayName: '',
|
|
serverName: '',
|
|
databaseName: '',
|
|
ownerUri: '',
|
|
packageFilePath: '',
|
|
connectionDetails: undefined
|
|
};
|
|
}
|
|
|
|
TelemetryReporter.createActionEvent(TelemetryViews.SchemaCompareDialog, 'SchemaCompareStart')
|
|
.withAdditionalProperties({
|
|
sourceEndpointType: getSchemaCompareEndpointString(this.sourceEndpointType),
|
|
targetEndpointType: getSchemaCompareEndpointString(this.targetEndpointType)
|
|
}).send();
|
|
|
|
// update source and target values that are displayed
|
|
this.schemaCompareMainWindow.updateSourceAndTarget();
|
|
|
|
const sourceEndpointChanged = this.endpointChanged(this.previousSource, this.schemaCompareMainWindow.sourceEndpointInfo);
|
|
const targetEndpointChanged = this.endpointChanged(this.previousTarget, this.schemaCompareMainWindow.targetEndpointInfo);
|
|
|
|
// show recompare message if it isn't the initial population of source and target
|
|
if (this.previousSource && this.previousTarget
|
|
&& (sourceEndpointChanged || targetEndpointChanged)) {
|
|
this.schemaCompareMainWindow.setButtonsForRecompare();
|
|
|
|
let message = loc.differentSourceMessage;
|
|
if (sourceEndpointChanged && targetEndpointChanged) {
|
|
message = loc.differentSourceTargetMessage;
|
|
} else if (targetEndpointChanged) {
|
|
message = loc.differentTargetMessage;
|
|
}
|
|
|
|
vscode.window.showWarningMessage(message, loc.YesButtonText, loc.NoButtonText).then((result) => {
|
|
if (result === loc.YesButtonText) {
|
|
this.schemaCompareMainWindow.startCompare();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
private endpointChanged(previousEndpoint: mssql.SchemaCompareEndpointInfo, updatedEndpoint: mssql.SchemaCompareEndpointInfo): boolean {
|
|
if (previousEndpoint && updatedEndpoint) {
|
|
return getEndpointName(previousEndpoint).toLowerCase() !== getEndpointName(updatedEndpoint).toLowerCase()
|
|
|| (previousEndpoint.serverDisplayName && updatedEndpoint.serverDisplayName && previousEndpoint.serverDisplayName.toLowerCase() !== updatedEndpoint.serverDisplayName.toLowerCase());
|
|
}
|
|
return false;
|
|
}
|
|
|
|
protected async cancel(): Promise<void> {
|
|
this.dispose();
|
|
}
|
|
|
|
private async initializeSchemaCompareTab(): Promise<void> {
|
|
this.schemaCompareTab.registerContent(async view => {
|
|
if (isNullOrUndefined(this.view)) {
|
|
this.view = view;
|
|
}
|
|
|
|
let sourceValue = '';
|
|
|
|
if (this.schemaCompareMainWindow.sourceEndpointInfo && this.schemaCompareMainWindow.sourceEndpointInfo.endpointType === mssql.SchemaCompareEndpointType.Dacpac) {
|
|
sourceValue = this.schemaCompareMainWindow.sourceEndpointInfo.packageFilePath;
|
|
} else if (this.schemaCompareMainWindow.sourceEndpointInfo && this.schemaCompareMainWindow.sourceEndpointInfo.endpointType === mssql.SchemaCompareEndpointType.Project) {
|
|
sourceValue = this.schemaCompareMainWindow.sourceEndpointInfo.projectFilePath;
|
|
}
|
|
|
|
this.sourceTextBox = this.view.modelBuilder.inputBox().withProps({
|
|
value: sourceValue,
|
|
width: this.textBoxWidth,
|
|
ariaLabel: loc.sourceFile
|
|
}).component();
|
|
|
|
this.sourceTextBox.onTextChanged(async (e) => {
|
|
this.dialog.okButton.enabled = await this.shouldEnableOkayButton();
|
|
|
|
if (this.sourceEndpointType === mssql.SchemaCompareEndpointType.Dacpac) {
|
|
this.sourceDacpacPath = e;
|
|
} else if (this.sourceEndpointType === mssql.SchemaCompareEndpointType.Project) {
|
|
this.sourceProjectFilePath = e;
|
|
}
|
|
});
|
|
|
|
let targetValue = '';
|
|
|
|
if (this.schemaCompareMainWindow.targetEndpointInfo && this.schemaCompareMainWindow.targetEndpointInfo.endpointType === mssql.SchemaCompareEndpointType.Dacpac) {
|
|
targetValue = this.schemaCompareMainWindow.targetEndpointInfo.packageFilePath;
|
|
} else if (this.schemaCompareMainWindow.targetEndpointInfo && this.schemaCompareMainWindow.targetEndpointInfo.endpointType === mssql.SchemaCompareEndpointType.Project) {
|
|
targetValue = this.schemaCompareMainWindow.targetEndpointInfo.projectFilePath;
|
|
}
|
|
|
|
this.targetTextBox = this.view.modelBuilder.inputBox().withProps({
|
|
value: targetValue,
|
|
width: this.textBoxWidth,
|
|
ariaLabel: loc.targetFile
|
|
}).component();
|
|
|
|
this.targetTextBox.onTextChanged(async (e) => {
|
|
this.dialog.okButton.enabled = await this.shouldEnableOkayButton();
|
|
|
|
if (this.targetEndpointType === mssql.SchemaCompareEndpointType.Dacpac) {
|
|
this.targetDacpacPath = e;
|
|
} else if (this.targetEndpointType === mssql.SchemaCompareEndpointType.Project) {
|
|
this.targetProjectFilePath = e;
|
|
}
|
|
});
|
|
|
|
this.sourceServerComponent = this.createSourceServerDropdown();
|
|
this.sourceDatabaseComponent = this.createSourceDatabaseDropdown();
|
|
|
|
this.targetServerComponent = this.createTargetServerDropdown();
|
|
this.targetDatabaseComponent = this.createTargetDatabaseDropdown();
|
|
|
|
this.sourceDacpacComponent = this.createFileBrowser(false, true, this.schemaCompareMainWindow.sourceEndpointInfo);
|
|
this.targetDacpacComponent = this.createFileBrowser(true, true, this.schemaCompareMainWindow.targetEndpointInfo);
|
|
|
|
this.sourceProjectFilePathComponent = this.createFileBrowser(false, false, this.schemaCompareMainWindow.sourceEndpointInfo);
|
|
this.targetProjectFilePathComponent = this.createFileBrowser(true, false, this.schemaCompareMainWindow.targetEndpointInfo);
|
|
|
|
this.targetProjectStructureComponent = this.createStructureDropdown();
|
|
|
|
let sourceRadioButtons = this.createSourceRadioButtons();
|
|
let targetRadioButtons = this.createTargetRadioButtons();
|
|
|
|
let sourceComponents = [];
|
|
let targetComponents = [];
|
|
|
|
// start source and target with either dacpac, database, or project selection based on what the previous value was
|
|
sourceComponents = [sourceRadioButtons];
|
|
|
|
switch (this.sourceEndpointType) {
|
|
case mssql.SchemaCompareEndpointType.Database:
|
|
sourceComponents.push(
|
|
this.sourceServerComponent,
|
|
this.sourceDatabaseComponent);
|
|
break;
|
|
case mssql.SchemaCompareEndpointType.Dacpac:
|
|
sourceComponents.push(this.sourceDacpacComponent);
|
|
break;
|
|
case mssql.SchemaCompareEndpointType.Project:
|
|
sourceComponents.push(this.sourceProjectFilePathComponent);
|
|
break;
|
|
}
|
|
|
|
targetComponents = [targetRadioButtons];
|
|
|
|
switch (this.targetEndpointType) {
|
|
case mssql.SchemaCompareEndpointType.Database:
|
|
targetComponents.push(
|
|
this.targetServerComponent,
|
|
this.targetDatabaseComponent);
|
|
break;
|
|
case mssql.SchemaCompareEndpointType.Dacpac:
|
|
targetComponents.push(this.targetDacpacComponent);
|
|
break;
|
|
case mssql.SchemaCompareEndpointType.Project:
|
|
targetComponents.push(this.targetProjectFilePathComponent);
|
|
break;
|
|
}
|
|
|
|
this.formBuilder = <azdata.FormBuilder>this.view.modelBuilder.formContainer()
|
|
.withFormItems([
|
|
{
|
|
title: loc.SourceTitle,
|
|
components: sourceComponents
|
|
}, {
|
|
title: loc.TargetTitle,
|
|
components: targetComponents
|
|
}
|
|
], {
|
|
horizontal: true,
|
|
titleFontSize: titleFontSize
|
|
})
|
|
.withLayout({
|
|
width: '100%',
|
|
padding: '10px 10px 0 30px'
|
|
});
|
|
|
|
let formModel = this.formBuilder.component();
|
|
await this.view.initializeModel(formModel);
|
|
|
|
switch (this.sourceEndpointType) {
|
|
case (mssql.SchemaCompareEndpointType.Database):
|
|
await this.sourceDatabaseRadioButton.focus();
|
|
break;
|
|
case (mssql.SchemaCompareEndpointType.Dacpac):
|
|
await this.sourceDacpacRadioButton.focus();
|
|
break;
|
|
case (mssql.SchemaCompareEndpointType.Project):
|
|
await this.sourceProjectRadioButton.focus();
|
|
break;
|
|
}
|
|
|
|
this.initDialogComplete.resolve();
|
|
});
|
|
}
|
|
|
|
private createFileBrowser(isTarget: boolean, dacpac: boolean, endpoint: mssql.SchemaCompareEndpointInfo): azdata.FormComponent {
|
|
let currentTextbox = isTarget ? this.targetTextBox : this.sourceTextBox;
|
|
|
|
if (isTarget) {
|
|
this.targetFileButton = this.view.modelBuilder.button().withProps({
|
|
title: loc.selectTargetFile,
|
|
ariaLabel: loc.selectTargetFile,
|
|
secondary: true,
|
|
iconPath: path.join(this.extensionContext.extensionPath, 'media', 'folder.svg')
|
|
}).component();
|
|
} else {
|
|
this.sourceFileButton = this.view.modelBuilder.button().withProps({
|
|
title: loc.selectSourceFile,
|
|
ariaLabel: loc.selectSourceFile,
|
|
secondary: true,
|
|
iconPath: path.join(this.extensionContext.extensionPath, 'media', 'folder.svg')
|
|
}).component();
|
|
}
|
|
|
|
let currentButton = isTarget ? this.targetFileButton : this.sourceFileButton;
|
|
const filter = dacpac ? 'dacpac' : 'sqlproj';
|
|
|
|
currentButton.onDidClick(async () => {
|
|
// file browser should open where the current dacpac is or the appropriate default folder
|
|
let rootPath = getRootPath();
|
|
let defaultUri = endpoint && endpoint.packageFilePath && await exists(endpoint.packageFilePath) ? endpoint.packageFilePath : rootPath;
|
|
|
|
let fileUris = await vscode.window.showOpenDialog(
|
|
{
|
|
canSelectFiles: true,
|
|
canSelectFolders: false,
|
|
canSelectMany: false,
|
|
defaultUri: vscode.Uri.file(defaultUri),
|
|
openLabel: loc.open,
|
|
filters: {
|
|
'Files': [filter],
|
|
}
|
|
}
|
|
);
|
|
|
|
if (!fileUris || fileUris.length === 0) {
|
|
return;
|
|
}
|
|
|
|
let fileUri = fileUris[0];
|
|
currentTextbox.value = fileUri.fsPath;
|
|
});
|
|
|
|
return {
|
|
component: currentTextbox,
|
|
title: loc.FileTextBoxLabel,
|
|
actions: [currentButton]
|
|
};
|
|
}
|
|
|
|
private createStructureDropdown(): azdata.FormComponent {
|
|
this.targetStructureDropdown = this.view.modelBuilder.dropDown().withProps({
|
|
editable: true,
|
|
fireOnTextChange: true,
|
|
ariaLabel: loc.targetStructure,
|
|
width: this.textBoxWidth,
|
|
values: [loc.file, loc.flat, loc.objectType, loc.schema, loc.schemaObjectType],
|
|
value: loc.schemaObjectType,
|
|
}).component();
|
|
|
|
return {
|
|
component: this.targetStructureDropdown,
|
|
title: loc.StructureDropdownLabel,
|
|
};
|
|
}
|
|
|
|
private createSourceRadioButtons(): azdata.FormComponent {
|
|
this.sourceDacpacRadioButton = this.view.modelBuilder.radioButton()
|
|
.withProps({
|
|
name: 'source',
|
|
label: loc.DacpacRadioButtonLabel
|
|
}).component();
|
|
|
|
this.sourceDatabaseRadioButton = this.view.modelBuilder.radioButton()
|
|
.withProps({
|
|
name: 'source',
|
|
label: loc.DatabaseRadioButtonLabel
|
|
}).component();
|
|
|
|
this.sourceProjectRadioButton = this.view.modelBuilder.radioButton()
|
|
.withProps({
|
|
name: 'source',
|
|
label: loc.ProjectRadioButtonLabel
|
|
}).component();
|
|
|
|
// show dacpac file browser
|
|
this.sourceDacpacRadioButton.onDidClick(async () => {
|
|
this.sourceEndpointType = mssql.SchemaCompareEndpointType.Dacpac;
|
|
this.sourceTextBox.value = this.sourceDacpacPath;
|
|
this.formBuilder.removeFormItem(this.sourceServerComponent);
|
|
this.formBuilder.removeFormItem(this.sourceDatabaseComponent);
|
|
this.formBuilder.removeFormItem(this.sourceProjectFilePathComponent);
|
|
this.formBuilder.insertFormItem(this.sourceDacpacComponent, 2, { horizontal: true, titleFontSize: titleFontSize });
|
|
this.dialog.okButton.enabled = await this.shouldEnableOkayButton();
|
|
});
|
|
|
|
// show server and db dropdowns
|
|
this.sourceDatabaseRadioButton.onDidClick(async () => {
|
|
this.sourceEndpointType = mssql.SchemaCompareEndpointType.Database;
|
|
this.formBuilder.insertFormItem(this.sourceServerComponent, 2, { horizontal: true, titleFontSize: titleFontSize });
|
|
this.formBuilder.insertFormItem(this.sourceDatabaseComponent, 3, { horizontal: true, titleFontSize: titleFontSize });
|
|
this.formBuilder.removeFormItem(this.sourceDacpacComponent);
|
|
this.formBuilder.removeFormItem(this.sourceProjectFilePathComponent);
|
|
|
|
await this.populateServerDropdown(false);
|
|
});
|
|
|
|
// show project directory browser
|
|
this.sourceProjectRadioButton.onDidClick(async () => {
|
|
this.sourceEndpointType = mssql.SchemaCompareEndpointType.Project;
|
|
this.sourceTextBox.value = this.sourceProjectFilePath;
|
|
this.formBuilder.removeFormItem(this.sourceServerComponent);
|
|
this.formBuilder.removeFormItem(this.sourceDatabaseComponent);
|
|
this.formBuilder.removeFormItem(this.sourceDacpacComponent);
|
|
this.formBuilder.insertFormItem(this.sourceProjectFilePathComponent, 2, { horizontal: true, titleFontSize: titleFontSize });
|
|
this.dialog.okButton.enabled = await this.shouldEnableOkayButton();
|
|
});
|
|
|
|
this.sourceEndpointType = this.schemaCompareMainWindow.sourceEndpointInfo?.endpointType ?? mssql.SchemaCompareEndpointType.Database; // default to database if no specific source is passed
|
|
|
|
switch (this.sourceEndpointType) {
|
|
case mssql.SchemaCompareEndpointType.Dacpac:
|
|
this.sourceDacpacRadioButton.checked = true;
|
|
break;
|
|
case mssql.SchemaCompareEndpointType.Project:
|
|
this.sourceProjectRadioButton.checked = true;
|
|
break;
|
|
case mssql.SchemaCompareEndpointType.Database:
|
|
this.sourceDatabaseRadioButton.checked = true;
|
|
break;
|
|
}
|
|
|
|
let radioButtons = [this.sourceDatabaseRadioButton, this.sourceDacpacRadioButton];
|
|
|
|
if (vscode.extensions.getExtension(loc.sqlDatabaseProjectExtensionId)) {
|
|
radioButtons.push(this.sourceProjectRadioButton);
|
|
}
|
|
|
|
let flexRadioButtonsModel = this.view.modelBuilder.flexContainer()
|
|
.withLayout({ flexFlow: 'column' })
|
|
.withItems(radioButtons)
|
|
.withProps({ ariaRole: 'radiogroup' })
|
|
.component();
|
|
|
|
return {
|
|
component: flexRadioButtonsModel,
|
|
title: loc.RadioButtonsLabel
|
|
};
|
|
}
|
|
|
|
private createTargetRadioButtons(): azdata.FormComponent {
|
|
let targetDacpacRadioButton = this.view.modelBuilder.radioButton()
|
|
.withProps({
|
|
name: 'target',
|
|
label: loc.DacpacRadioButtonLabel
|
|
}).component();
|
|
|
|
let targetDatabaseRadioButton = this.view.modelBuilder.radioButton()
|
|
.withProps({
|
|
name: 'target',
|
|
label: loc.DatabaseRadioButtonLabel
|
|
}).component();
|
|
|
|
let targetProjectRadioButton = this.view.modelBuilder.radioButton()
|
|
.withProps({
|
|
name: 'target',
|
|
label: loc.ProjectRadioButtonLabel
|
|
}).component();
|
|
|
|
// show dacpac file browser
|
|
targetDacpacRadioButton.onDidClick(async () => {
|
|
this.targetEndpointType = mssql.SchemaCompareEndpointType.Dacpac;
|
|
this.targetTextBox.value = this.targetDacpacPath;
|
|
this.formBuilder.removeFormItem(this.targetServerComponent);
|
|
this.formBuilder.removeFormItem(this.targetDatabaseComponent);
|
|
this.formBuilder.removeFormItem(this.targetProjectFilePathComponent);
|
|
this.formBuilder.removeFormItem(this.targetProjectStructureComponent);
|
|
this.formBuilder.addFormItem(this.targetDacpacComponent, { horizontal: true, titleFontSize: titleFontSize });
|
|
this.dialog.okButton.enabled = await this.shouldEnableOkayButton();
|
|
});
|
|
|
|
// show server and db dropdowns
|
|
targetDatabaseRadioButton.onDidClick(async () => {
|
|
this.targetEndpointType = mssql.SchemaCompareEndpointType.Database;
|
|
this.formBuilder.removeFormItem(this.targetDacpacComponent);
|
|
this.formBuilder.removeFormItem(this.targetProjectFilePathComponent);
|
|
this.formBuilder.removeFormItem(this.targetProjectStructureComponent);
|
|
this.formBuilder.addFormItem(this.targetServerComponent, { horizontal: true, titleFontSize: titleFontSize });
|
|
this.formBuilder.addFormItem(this.targetDatabaseComponent, { horizontal: true, titleFontSize: titleFontSize });
|
|
|
|
await this.populateServerDropdown(true);
|
|
});
|
|
|
|
// show project directory browser
|
|
targetProjectRadioButton.onDidClick(async () => {
|
|
this.targetEndpointType = mssql.SchemaCompareEndpointType.Project;
|
|
this.targetTextBox.value = this.targetProjectFilePath;
|
|
this.formBuilder.removeFormItem(this.targetServerComponent);
|
|
this.formBuilder.removeFormItem(this.targetDatabaseComponent);
|
|
this.formBuilder.removeFormItem(this.targetDacpacComponent);
|
|
this.formBuilder.addFormItem(this.targetProjectFilePathComponent, { horizontal: true, titleFontSize: titleFontSize });
|
|
this.formBuilder.addFormItem(this.targetProjectStructureComponent, { horizontal: true, titleFontSize: titleFontSize });
|
|
this.dialog.okButton.enabled = await this.shouldEnableOkayButton();
|
|
});
|
|
|
|
|
|
this.targetEndpointType = this.schemaCompareMainWindow.targetEndpointInfo?.endpointType ?? mssql.SchemaCompareEndpointType.Database; // default to database if no specific target is passed
|
|
|
|
switch (this.targetEndpointType) {
|
|
case mssql.SchemaCompareEndpointType.Dacpac:
|
|
targetDacpacRadioButton.checked = true;
|
|
break;
|
|
case mssql.SchemaCompareEndpointType.Project:
|
|
targetProjectRadioButton.checked = true;
|
|
break;
|
|
case mssql.SchemaCompareEndpointType.Database:
|
|
targetDatabaseRadioButton.checked = true;
|
|
break;
|
|
}
|
|
|
|
let radioButtons = [targetDatabaseRadioButton, targetDacpacRadioButton];
|
|
|
|
if (vscode.extensions.getExtension(loc.sqlDatabaseProjectExtensionId)) {
|
|
radioButtons.push(targetProjectRadioButton);
|
|
}
|
|
|
|
let flexRadioButtonsModel = this.view.modelBuilder.flexContainer()
|
|
.withLayout({ flexFlow: 'column' })
|
|
.withItems(radioButtons)
|
|
.withProps({ ariaRole: 'radiogroup' })
|
|
.component();
|
|
|
|
return {
|
|
component: flexRadioButtonsModel,
|
|
title: loc.RadioButtonsLabel
|
|
};
|
|
}
|
|
|
|
private async shouldEnableOkayButton(): Promise<boolean> {
|
|
let sourcefilled = (this.sourceEndpointType === mssql.SchemaCompareEndpointType.Dacpac && await this.existsDacpac(this.sourceTextBox.value))
|
|
|| (this.sourceEndpointType === mssql.SchemaCompareEndpointType.Project && await this.existsProjectFile(this.sourceTextBox.value))
|
|
|| (this.sourceEndpointType === mssql.SchemaCompareEndpointType.Database && !isNullOrUndefined(this.sourceDatabaseDropdown.value) && this.sourceDatabaseDropdown.values.findIndex(x => this.matchesValue(x, this.sourceDbEditable)) !== -1);
|
|
let targetfilled = (this.targetEndpointType === mssql.SchemaCompareEndpointType.Dacpac && await this.existsDacpac(this.targetTextBox.value))
|
|
|| (this.targetEndpointType === mssql.SchemaCompareEndpointType.Project && await this.existsProjectFile(this.targetTextBox.value))
|
|
|| (this.targetEndpointType === mssql.SchemaCompareEndpointType.Database && !isNullOrUndefined(this.targetDatabaseDropdown.value) && this.targetDatabaseDropdown.values.findIndex(x => this.matchesValue(x, this.targetDbEditable)) !== -1);
|
|
|
|
return sourcefilled && targetfilled;
|
|
}
|
|
|
|
public async handleOkButtonClick(): Promise<void> {
|
|
await this.execute();
|
|
this.dispose();
|
|
}
|
|
|
|
protected showErrorMessage(message: string): void {
|
|
this.dialog.message = {
|
|
text: message,
|
|
level: getAzdataApi()!.window.MessageLevel.Error
|
|
};
|
|
}
|
|
|
|
async validate(): Promise<boolean> {
|
|
try {
|
|
// check project extension is installed
|
|
if (!vscode.extensions.getExtension(loc.sqlDatabaseProjectExtensionId) &&
|
|
(this.sourceEndpointType === mssql.SchemaCompareEndpointType.Project ||
|
|
this.targetEndpointType === mssql.SchemaCompareEndpointType.Project)) {
|
|
this.showErrorMessage(loc.noProjectExtension);
|
|
return false;
|
|
}
|
|
|
|
// check Database Schema Providers are set and valid
|
|
if (this.sourceEndpointType === mssql.SchemaCompareEndpointType.Project) {
|
|
try {
|
|
await this.getDatabaseSchemaProvider(this.sourceTextBox.value);
|
|
} catch (err) {
|
|
this.showErrorMessage(loc.dspErrorSource);
|
|
}
|
|
}
|
|
|
|
if (this.targetEndpointType === mssql.SchemaCompareEndpointType.Project) {
|
|
try {
|
|
await this.getDatabaseSchemaProvider(this.targetTextBox.value);
|
|
} catch (err) {
|
|
this.showErrorMessage(loc.dspErrorTarget);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
} catch (e) {
|
|
this.showErrorMessage(e?.message ? e.message : e);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private dispose(): void {
|
|
this.toDispose.forEach(disposable => disposable.dispose());
|
|
}
|
|
|
|
private async existsDacpac(filename: string): Promise<boolean> {
|
|
return !isNullOrUndefined(filename) && await exists(filename) && (filename.toLocaleLowerCase().endsWith('.dacpac'));
|
|
}
|
|
|
|
private async existsProjectFile(filename: string): Promise<boolean> {
|
|
return !isNullOrUndefined(filename) && await exists(filename) && (filename.toLocaleLowerCase().endsWith('.sqlproj'));
|
|
}
|
|
|
|
private async getProjectScriptFiles(projectFilePath: string): Promise<string[]> {
|
|
const databaseProjectsExtension = vscode.extensions.getExtension(loc.sqlDatabaseProjectExtensionId);
|
|
|
|
if (databaseProjectsExtension) {
|
|
return await (await databaseProjectsExtension.activate() as sqldbproj.IExtension).getProjectScriptFiles(projectFilePath);
|
|
}
|
|
}
|
|
|
|
private async getDatabaseSchemaProvider(projectFilePath: string): Promise<string> {
|
|
const databaseProjectsExtension = vscode.extensions.getExtension(loc.sqlDatabaseProjectExtensionId);
|
|
|
|
if (databaseProjectsExtension) {
|
|
return await (await databaseProjectsExtension.activate() as sqldbproj.IExtension).getProjectDatabaseSchemaProvider(projectFilePath);
|
|
}
|
|
}
|
|
|
|
protected createSourceServerDropdown(): azdata.FormComponent {
|
|
this.sourceServerDropdown = this.view.modelBuilder.dropDown().withProps(
|
|
{
|
|
editable: true,
|
|
fireOnTextChange: true,
|
|
ariaLabel: loc.sourceServer,
|
|
width: this.textBoxWidth
|
|
}
|
|
).component();
|
|
|
|
this.sourceConnectionButton = this.createConnectionButton(false);
|
|
|
|
this.sourceServerDropdown.onValueChanged(async (value) => {
|
|
if (value.selected && this.sourceServerDropdown.values.findIndex(x => this.matchesValue(x, value.selected)) === -1) {
|
|
await this.sourceDatabaseDropdown.updateProperties({
|
|
values: [],
|
|
value: ' '
|
|
});
|
|
}
|
|
else {
|
|
this.sourceConnectionButton.iconPath = path.join(this.extensionContext.extensionPath, 'media', 'connect.svg');
|
|
await this.populateDatabaseDropdown((this.sourceServerDropdown.value as ConnectionDropdownValue).connection, false);
|
|
}
|
|
});
|
|
|
|
// don't await so that dialog loading won't be blocked. Dropdown will show loading indicator until it is populated
|
|
this.populateServerDropdown(false);
|
|
|
|
return {
|
|
component: this.sourceServerDropdown,
|
|
title: loc.ServerDropdownLabel,
|
|
actions: [this.sourceConnectionButton]
|
|
};
|
|
}
|
|
|
|
private createConnectionButton(isTarget: boolean): azdata.ButtonComponent {
|
|
const selectConnectionButton = this.view.modelBuilder.button().withProps({
|
|
ariaLabel: loc.selectConnection,
|
|
iconPath: path.join(this.extensionContext.extensionPath, 'media', 'selectConnection.svg'),
|
|
height: '20px',
|
|
width: '20px'
|
|
}).component();
|
|
|
|
selectConnectionButton.onDidClick(async () => {
|
|
await this.connectionButtonClick(isTarget);
|
|
selectConnectionButton.iconPath = path.join(this.extensionContext.extensionPath, 'media', 'connect.svg');
|
|
});
|
|
|
|
return selectConnectionButton;
|
|
}
|
|
|
|
public async connectionButtonClick(isTarget: boolean): Promise<void> {
|
|
let connection = await azdata.connection.openConnectionDialog();
|
|
if (connection) {
|
|
this.connectionId = connection.connectionId;
|
|
this.promise = this.populateServerDropdown(isTarget);
|
|
this.promise2 = this.populateServerDropdown(!isTarget, true); // passively populate the other server dropdown as well to add the new connections
|
|
}
|
|
}
|
|
|
|
protected createTargetServerDropdown(): azdata.FormComponent {
|
|
this.targetServerDropdown = this.view.modelBuilder.dropDown().withProps(
|
|
{
|
|
editable: true,
|
|
fireOnTextChange: true,
|
|
ariaLabel: loc.targetServer,
|
|
width: this.textBoxWidth
|
|
}
|
|
).component();
|
|
this.targetConnectionButton = this.createConnectionButton(true);
|
|
this.targetServerDropdown.onValueChanged(async (value) => {
|
|
if (value.selected && this.targetServerDropdown.values.findIndex(x => this.matchesValue(x, value.selected)) === -1) {
|
|
await this.targetDatabaseDropdown.updateProperties({
|
|
values: [],
|
|
value: ' '
|
|
});
|
|
}
|
|
else {
|
|
this.targetConnectionButton.iconPath = path.join(this.extensionContext.extensionPath, 'media', 'connect.svg');
|
|
await this.populateDatabaseDropdown((this.targetServerDropdown.value as ConnectionDropdownValue).connection, true);
|
|
}
|
|
});
|
|
// don't await so that dialog loading won't be blocked. Dropdown will show loading indicator until it is populated
|
|
this.populateServerDropdown(true);
|
|
return {
|
|
component: this.targetServerDropdown,
|
|
title: loc.ServerDropdownLabel,
|
|
actions: [this.targetConnectionButton]
|
|
};
|
|
}
|
|
|
|
protected async populateServerDropdown(isTarget: boolean, passivelyPopulate: boolean = false): Promise<void> {
|
|
const currentDropdown = isTarget ? this.targetServerDropdown : this.sourceServerDropdown;
|
|
|
|
if (passivelyPopulate && isNullOrUndefined(currentDropdown.value)) {
|
|
passivelyPopulate = false; // Populate the dropdown if it is empty
|
|
}
|
|
|
|
currentDropdown.loading = true;
|
|
const values = await this.getServerValues(isTarget);
|
|
|
|
if (values && values.length > 0) {
|
|
if (passivelyPopulate) { // only update the dropdown values, not the selected value
|
|
await currentDropdown.updateProperties({
|
|
values: values
|
|
});
|
|
} else {
|
|
await currentDropdown.updateProperties({
|
|
values: values,
|
|
value: values[0]
|
|
});
|
|
}
|
|
}
|
|
|
|
currentDropdown.loading = false;
|
|
|
|
if (!passivelyPopulate && currentDropdown.value) {
|
|
await this.populateDatabaseDropdown((currentDropdown.value as ConnectionDropdownValue).connection, isTarget);
|
|
}
|
|
}
|
|
|
|
protected async getServerValues(isTarget: boolean): Promise<{ connection: azdata.connection.ConnectionProfile, displayName: string, name: string }[]> {
|
|
let cons = await azdata.connection.getConnections(/* activeConnectionsOnly */ true);
|
|
// This user has no active connections
|
|
if (!cons || cons.length === 0) {
|
|
return undefined;
|
|
}
|
|
|
|
// Update connection icon to "connected" state
|
|
let connectionButton = isTarget ? this.targetConnectionButton : this.sourceConnectionButton;
|
|
connectionButton.iconPath = path.join(this.extensionContext.extensionPath, 'media', 'connect.svg');
|
|
|
|
let endpointInfo = isTarget ? this.schemaCompareMainWindow.targetEndpointInfo : this.schemaCompareMainWindow.sourceEndpointInfo;
|
|
// reverse list so that most recent connections are first
|
|
cons.reverse();
|
|
|
|
let count = -1;
|
|
let idx = -1;
|
|
let values = cons.map(c => {
|
|
count++;
|
|
|
|
let usr = c.options.user;
|
|
|
|
if (!usr) {
|
|
usr = loc.defaultText;
|
|
}
|
|
|
|
let srv = c.options.server;
|
|
|
|
let finalName = `${srv} (${usr})`;
|
|
|
|
if (c.options.connectionName) {
|
|
finalName = c.options.connectionName;
|
|
}
|
|
|
|
// use previously selected server or current connection if there is one
|
|
if (endpointInfo && !isNullOrUndefined(endpointInfo.serverName) && !isNullOrUndefined(endpointInfo.serverDisplayName)
|
|
&& c.options.server.toLowerCase() === endpointInfo.serverName.toLowerCase()
|
|
&& finalName.toLowerCase() === endpointInfo.serverDisplayName.toLowerCase()) {
|
|
idx = count;
|
|
}
|
|
else if (c.connectionId === this.connectionId) {
|
|
idx = count;
|
|
}
|
|
|
|
return {
|
|
connection: c,
|
|
displayName: finalName,
|
|
name: srv,
|
|
user: usr
|
|
};
|
|
});
|
|
|
|
// move server of current connection to the top of the list so it is the default
|
|
if (idx >= 1) {
|
|
let tmp = values[0];
|
|
values[0] = values[idx];
|
|
values[idx] = tmp;
|
|
}
|
|
|
|
values = values.reduce((uniqueValues, conn) => {
|
|
let exists = uniqueValues.find(x => x.displayName === conn.displayName);
|
|
if (!exists) {
|
|
uniqueValues.push(conn);
|
|
}
|
|
return uniqueValues;
|
|
}, []);
|
|
|
|
return values;
|
|
}
|
|
|
|
protected createSourceDatabaseDropdown(): azdata.FormComponent {
|
|
this.sourceDatabaseDropdown = this.view.modelBuilder.dropDown().withProps(
|
|
{
|
|
editable: true,
|
|
fireOnTextChange: true,
|
|
ariaLabel: loc.sourceDatabase,
|
|
width: this.textBoxWidth
|
|
}
|
|
).component();
|
|
this.sourceDatabaseDropdown.onValueChanged(async (value) => {
|
|
this.sourceDbEditable = value as string;
|
|
this.dialog.okButton.enabled = await this.shouldEnableOkayButton();
|
|
});
|
|
|
|
return {
|
|
component: this.sourceDatabaseDropdown,
|
|
title: loc.DatabaseDropdownLabel
|
|
};
|
|
}
|
|
|
|
protected createTargetDatabaseDropdown(): azdata.FormComponent {
|
|
this.targetDatabaseDropdown = this.view.modelBuilder.dropDown().withProps(
|
|
{
|
|
editable: true,
|
|
fireOnTextChange: true,
|
|
ariaLabel: loc.targetDatabase,
|
|
width: this.textBoxWidth
|
|
}
|
|
).component();
|
|
this.targetDatabaseDropdown.onValueChanged(async (value) => {
|
|
this.targetDbEditable = value as string;
|
|
this.dialog.okButton.enabled = await this.shouldEnableOkayButton();
|
|
});
|
|
|
|
return {
|
|
component: this.targetDatabaseDropdown,
|
|
title: loc.DatabaseDropdownLabel
|
|
};
|
|
}
|
|
|
|
private matchesValue(listValue: any, value: string): boolean {
|
|
return listValue.displayName === value || listValue === value;
|
|
}
|
|
|
|
protected async populateDatabaseDropdown(connectionProfile: azdata.connection.ConnectionProfile, isTarget: boolean): Promise<void> {
|
|
const currentDropdown = isTarget ? this.targetDatabaseDropdown : this.sourceDatabaseDropdown;
|
|
currentDropdown.loading = true;
|
|
await currentDropdown.updateProperties({
|
|
values: [],
|
|
value: undefined
|
|
});
|
|
|
|
let values = [];
|
|
try {
|
|
values = await this.getDatabaseValues(connectionProfile.connectionId, isTarget);
|
|
} catch (e) {
|
|
// if the user doesn't have access to master, just set the database of the connection profile
|
|
values = [connectionProfile.databaseName];
|
|
console.warn(e);
|
|
}
|
|
if (values && values.length > 0) {
|
|
await currentDropdown.updateProperties({
|
|
values: values,
|
|
value: values[0],
|
|
});
|
|
}
|
|
|
|
this.dialog.okButton.enabled = await this.shouldEnableOkayButton();
|
|
currentDropdown.loading = false;
|
|
}
|
|
|
|
protected async getDatabaseValues(connectionId: string, isTarget: boolean): Promise<string[]> {
|
|
let endpointInfo = isTarget ? this.schemaCompareMainWindow.targetEndpointInfo : this.schemaCompareMainWindow.sourceEndpointInfo;
|
|
|
|
let idx = -1;
|
|
let count = -1;
|
|
let values = (await azdata.connection.listDatabases(connectionId)).sort((a, b) => a.localeCompare(b)).map(db => {
|
|
count++;
|
|
|
|
// put currently selected db at the top of the dropdown if there is one
|
|
if (endpointInfo && endpointInfo.databaseName !== null
|
|
&& db === endpointInfo.databaseName) {
|
|
idx = count;
|
|
}
|
|
|
|
return db;
|
|
});
|
|
|
|
if (idx >= 0) {
|
|
let tmp = values[0];
|
|
values[0] = values[idx];
|
|
values[idx] = tmp;
|
|
}
|
|
return values;
|
|
}
|
|
}
|
|
|
|
export interface ConnectionDropdownValue extends azdata.CategoryValue {
|
|
connection: azdata.connection.ConnectionProfile;
|
|
}
|
|
|
|
function isNullOrUndefined(val: any): boolean {
|
|
return val === null || val === undefined;
|
|
}
|
|
|