Files
azuredatastudio/extensions/schema-compare/src/schemaCompareMainWindow.ts
Kim Santiago e3c347e148 Fixing folder path in dacpac and schema compare extensions (#7352)
* fixing folder path in dacpac and schema compare extensions

* created method

* import os
2019-09-25 15:00:56 -07:00

1049 lines
40 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 nls from 'vscode-nls';
import * as azdata from 'azdata';
import * as vscode from 'vscode';
import * as os from 'os';
import * as path from 'path';
import * as mssql from '../../mssql';
import { SchemaCompareOptionsDialog } from './dialogs/schemaCompareOptionsDialog';
import { Telemetry } from './telemetry';
import { getTelemetryErrorType, getEndpointName, verifyConnectionAndGetOwnerUri, getRootPath } from './utils';
import { SchemaCompareDialog } from './dialogs/schemaCompareDialog';
import { isNullOrUndefined } from 'util';
const localize = nls.loadMessageBundle();
const diffEditorTitle = localize('schemaCompare.CompareDetailsTitle', 'Compare Details');
const applyConfirmation = localize('schemaCompare.ApplyConfirmation', 'Are you sure you want to update the target?');
const reCompareToRefeshMessage = localize('schemaCompare.RecompareToRefresh', 'Press Compare to refresh the comparison.');
const generateScriptEnabledMessage = localize('schemaCompare.generateScriptEnabledButton', 'Generate script to deploy changes to target');
const generateScriptNoChangesMessage = localize('schemaCompare.generateScriptNoChanges', 'No changes to script');
const applyEnabledMessage = localize('schemaCompare.applyButtonEnabledTitle', 'Apply changes to target');
const applyNoChangesMessage = localize('schemaCompare.applyNoChanges', 'No changes to apply');
// Do not localize this, this is used to decide the icon for the editor.
// TODO : In future icon should be decided based on language id (scmp) and not resource name
const schemaCompareResourceName = 'Schema Compare';
enum ResetButtonState {
noSourceTarget,
beforeCompareStart,
comparing,
afterCompareComplete
}
export class SchemaCompareMainWindow {
private differencesTable: azdata.TableComponent;
private loader: azdata.LoadingComponent;
private startText: azdata.TextComponent;
private waitText: azdata.TextComponent;
private editor: azdata.workspace.ModelViewEditor;
private diffEditor: azdata.DiffEditorComponent;
private splitView: azdata.SplitViewContainer;
private flexModel: azdata.FlexContainer;
private noDifferencesLabel: azdata.TextComponent;
private sourceTargetFlexLayout: azdata.FlexContainer;
private switchButton: azdata.ButtonComponent;
private compareButton: azdata.ButtonComponent;
private cancelCompareButton: azdata.ButtonComponent;
private optionsButton: azdata.ButtonComponent;
private generateScriptButton: azdata.ButtonComponent;
private applyButton: azdata.ButtonComponent;
private openScmpButton: azdata.ButtonComponent;
private selectSourceButton: azdata.ButtonComponent;
private selectTargetButton: azdata.ButtonComponent;
private saveScmpButton: azdata.ButtonComponent;
private SchemaCompareActionMap: Map<Number, string>;
private operationId: string;
private comparisonResult: mssql.SchemaCompareResult;
private sourceNameComponent: azdata.TableComponent;
private targetNameComponent: azdata.TableComponent;
private deploymentOptions: mssql.DeploymentOptions;
private schemaCompareOptionDialog: SchemaCompareOptionsDialog;
private tablelistenersToDispose: vscode.Disposable[] = [];
private originalSourceExcludes = new Map<string, mssql.DiffEntry>();
private originalTargetExcludes = new Map<string, mssql.DiffEntry>();
private sourceTargetSwitched = false;
private sourceName: string;
private targetName: string;
private scmpSourceExcludes: mssql.SchemaCompareObjectId[];
private scmpTargetExcludes: mssql.SchemaCompareObjectId[];
public sourceEndpointInfo: mssql.SchemaCompareEndpointInfo;
public targetEndpointInfo: mssql.SchemaCompareEndpointInfo;
constructor(private schemaCompareService?: mssql.ISchemaCompareService, private extensionContext?: vscode.ExtensionContext) {
this.SchemaCompareActionMap = new Map<Number, string>();
this.SchemaCompareActionMap[mssql.SchemaUpdateAction.Delete] = localize('schemaCompare.deleteAction', 'Delete');
this.SchemaCompareActionMap[mssql.SchemaUpdateAction.Change] = localize('schemaCompare.changeAction', 'Change');
this.SchemaCompareActionMap[mssql.SchemaUpdateAction.Add] = localize('schemaCompare.addAction', 'Add');
this.editor = azdata.workspace.createModelViewEditor(localize('schemaCompare.Title', 'Schema Compare'), { retainContextWhenHidden: true, supportsSave: true, resourceName: schemaCompareResourceName });
}
public async start(context: any) {
// if schema compare was launched from a db, set that as the source
let profile = context ? <azdata.IConnectionProfile>context.connectionProfile : undefined;
if (profile) {
let ownerUri = await azdata.connection.getUriForConnection((profile.id));
this.sourceEndpointInfo = {
endpointType: mssql.SchemaCompareEndpointType.Database,
serverDisplayName: `${profile.serverName} ${profile.userName}`,
serverName: profile.serverName,
databaseName: profile.databaseName,
ownerUri: ownerUri,
packageFilePath: '',
connectionDetails: undefined
};
}
this.editor.registerContent(async view => {
this.differencesTable = view.modelBuilder.table().withProperties({
data: [],
height: 300,
title: localize('schemaCompare.differencesTableTitle', "Comparison between Source and Target")
}).component();
this.diffEditor = view.modelBuilder.diffeditor().withProperties({
contentLeft: os.EOL,
contentRight: os.EOL,
height: 500,
title: diffEditorTitle
}).component();
this.splitView = view.modelBuilder.splitViewContainer().component();
let sourceTargetLabels = view.modelBuilder.flexContainer()
.withProperties({
alignItems: 'stretch',
horizontal: true
}).component();
this.sourceTargetFlexLayout = view.modelBuilder.flexContainer()
.withProperties({
alignItems: 'stretch',
horizontal: true
}).component();
this.createSwitchButton(view);
this.createCompareButton(view);
this.createCancelButton(view);
this.createGenerateScriptButton(view);
this.createApplyButton(view);
this.createOptionsButton(view);
this.createOpenScmpButton(view);
this.createSaveScmpButton(view);
this.createSourceAndTargetButtons(view);
this.resetButtons(ResetButtonState.noSourceTarget);
let toolBar = view.modelBuilder.toolbarContainer();
toolBar.addToolbarItems([{
component: this.compareButton
}, {
component: this.cancelCompareButton
}, {
component: this.generateScriptButton
}, {
component: this.applyButton
}, {
component: this.optionsButton,
toolbarSeparatorAfter: true
}, {
component: this.switchButton,
toolbarSeparatorAfter: true
}, {
component: this.openScmpButton
}, {
component: this.saveScmpButton
}]);
let sourceLabel = view.modelBuilder.text().withProperties({
value: localize('schemaCompare.sourceLabel', 'Source'),
CSSStyles: { 'margin-bottom': '0px' }
}).component();
let targetLabel = view.modelBuilder.text().withProperties({
value: localize('schemaCompare.targetLabel', 'Target'),
CSSStyles: { 'margin-bottom': '0px' }
}).component();
let arrowLabel = view.modelBuilder.text().withProperties({
value: localize('schemaCompare.switchLabel', '➔')
}).component();
this.sourceName = getEndpointName(this.sourceEndpointInfo);
this.targetName = ' ';
this.sourceNameComponent = view.modelBuilder.table().withProperties({
columns: [
{
value: this.sourceName,
headerCssClass: 'no-borders',
toolTip: this.sourceName
},
]
}).component();
this.targetNameComponent = view.modelBuilder.table().withProperties({
columns: [
{
value: this.targetName,
headerCssClass: 'no-borders',
toolTip: this.targetName
},
]
}).component();
sourceTargetLabels.addItem(sourceLabel, { CSSStyles: { 'width': '55%', 'margin-left': '15px', 'font-size': 'larger', 'font-weight': 'bold' } });
sourceTargetLabels.addItem(targetLabel, { CSSStyles: { 'width': '45%', 'font-size': 'larger', 'font-weight': 'bold' } });
this.sourceTargetFlexLayout.addItem(this.sourceNameComponent, { CSSStyles: { 'width': '40%', 'height': '25px', 'margin-top': '10px', 'margin-left': '15px' } });
this.sourceTargetFlexLayout.addItem(this.selectSourceButton, { CSSStyles: { 'margin-top': '10px' } });
this.sourceTargetFlexLayout.addItem(arrowLabel, { CSSStyles: { 'width': '10%', 'font-size': 'larger', 'text-align-last': 'center' } });
this.sourceTargetFlexLayout.addItem(this.targetNameComponent, { CSSStyles: { 'width': '40%', 'height': '25px', 'margin-top': '10px', 'margin-left': '15px' } });
this.sourceTargetFlexLayout.addItem(this.selectTargetButton, { CSSStyles: { 'margin-top': '10px' } });
this.loader = view.modelBuilder.loadingComponent().component();
this.waitText = view.modelBuilder.text().withProperties({
value: localize('schemaCompare.waitText', 'Initializing Comparison. This might take a moment.')
}).component();
this.startText = view.modelBuilder.text().withProperties({
value: localize('schemaCompare.startText', 'To compare two schemas, first select a source schema and target schema, then press Compare.')
}).component();
this.noDifferencesLabel = view.modelBuilder.text().withProperties({
value: localize('schemaCompare.noDifferences', 'No schema differences were found.')
}).component();
this.flexModel = view.modelBuilder.flexContainer().component();
this.flexModel.addItem(toolBar.component(), { flex: 'none' });
this.flexModel.addItem(sourceTargetLabels, { flex: 'none' });
this.flexModel.addItem(this.sourceTargetFlexLayout, { flex: 'none' });
this.flexModel.addItem(this.startText, { CSSStyles: { 'margin': 'auto' } });
this.flexModel.setLayout({
flexFlow: 'column',
height: '100%'
});
await view.initializeModel(this.flexModel);
});
await this.GetDefaultDeploymentOptions();
this.editor.openEditor();
}
// update source and target name to display
public updateSourceAndTarget() {
this.sourceName = getEndpointName(this.sourceEndpointInfo);
this.targetName = getEndpointName(this.targetEndpointInfo);
this.sourceNameComponent.updateProperty('columns', [
{
value: this.sourceName,
headerCssClass: 'no-borders',
toolTip: this.sourceName
},
]);
this.targetNameComponent.updateProperty('columns', [
{
value: this.targetName,
headerCssClass: 'no-borders',
toolTip: this.targetName
},
]);
// reset buttons to before comparison state
this.resetButtons(ResetButtonState.beforeCompareStart);
}
// only for test
public getComparisonResult(): mssql.SchemaCompareResult {
return this.comparisonResult;
}
// only for test
public getDeploymentOptions(): mssql.DeploymentOptions {
return this.deploymentOptions;
}
public setDeploymentOptions(deploymentOptions: mssql.DeploymentOptions): void {
this.deploymentOptions = deploymentOptions;
}
public async execute(): Promise<void> {
Telemetry.sendTelemetryEvent('SchemaComparisonStarted');
const service = await this.getService();
if (!this.operationId) {
// create once per page
this.operationId = generateGuid();
}
this.comparisonResult = await service.schemaCompare(this.operationId, this.sourceEndpointInfo, this.targetEndpointInfo, azdata.TaskExecutionMode.execute, this.deploymentOptions);
if (!this.comparisonResult || !this.comparisonResult.success) {
Telemetry.sendTelemetryEventForError('SchemaComparisonFailed', {
'errorType': getTelemetryErrorType(this.comparisonResult.errorMessage),
'operationId': this.comparisonResult.operationId
});
vscode.window.showErrorMessage(localize('schemaCompare.compareErrorMessage', "Schema Compare failed: {0}", this.comparisonResult.errorMessage ? this.comparisonResult.errorMessage : 'Unknown'));
return;
}
Telemetry.sendTelemetryEvent('SchemaComparisonFinished', {
'endTime': Date.now().toString(),
'operationId': this.comparisonResult.operationId
});
let data = this.getAllDifferences(this.comparisonResult.differences);
this.differencesTable.updateProperties({
data: data,
columns: [
{
value: localize('schemaCompare.typeColumn', 'Type'),
cssClass: 'align-with-header',
width: 50
},
{
value: localize('schemaCompare.sourceNameColumn', 'Source Name'),
cssClass: 'align-with-header',
width: 90
},
{
value: localize('schemaCompare.includeColumnName', 'Include'),
cssClass: 'align-with-header',
width: 60,
type: azdata.ColumnType.checkBox,
options: { actionOnCheckbox: azdata.ActionOnCellCheckboxCheck.customAction }
},
{
value: localize('schemaCompare.actionColumn', 'Action'),
cssClass: 'align-with-header',
width: 30
},
{
value: localize('schemaCompare.targetNameColumn', 'Target Name'),
cssClass: 'align-with-header',
width: 150
}
],
CSSStyles: { 'left': '15px' },
width: '98%'
});
this.splitView.addItem(this.differencesTable);
this.splitView.addItem(this.diffEditor);
this.splitView.setLayout({
orientation: 'vertical',
splitViewHeight: 800
});
this.flexModel.removeItem(this.loader);
this.flexModel.removeItem(this.waitText);
this.resetButtons(ResetButtonState.afterCompareComplete);
if (this.comparisonResult.differences.length > 0) {
this.flexModel.addItem(this.splitView, { CSSStyles: { 'overflow': 'hidden' } });
// only enable generate script button if the target is a db
if (this.targetEndpointInfo.endpointType === mssql.SchemaCompareEndpointType.Database) {
this.generateScriptButton.enabled = true;
this.applyButton.enabled = true;
} else {
this.generateScriptButton.title = localize('schemaCompare.generateScriptButtonDisabledTitle', 'Generate script is enabled when the target is a database');
this.applyButton.title = localize('schemaCompare.applyButtonDisabledTitle', 'Apply is enabled when the target is a database');
}
} else {
this.flexModel.addItem(this.noDifferencesLabel, { CSSStyles: { 'margin': 'auto' } });
}
// explicitly exclude things that were excluded in previous compare
const thingsToExclude = this.sourceTargetSwitched ? this.originalTargetExcludes : this.originalSourceExcludes;
if (thingsToExclude) {
thingsToExclude.forEach(item => {
service.schemaCompareIncludeExcludeNode(this.comparisonResult.operationId, item, false, azdata.TaskExecutionMode.execute);
});
// disable apply and generate script buttons if no changes are included
if (thingsToExclude.size === this.comparisonResult.differences.length) {
this.setButtonStatesForNoChanges(false);
}
}
let sourceText = '';
let targetText = '';
this.tablelistenersToDispose.push(this.differencesTable.onRowSelected(() => {
let difference = this.comparisonResult.differences[this.differencesTable.selectedRows[0]];
if (difference !== undefined) {
sourceText = this.getFormattedScript(difference, true);
targetText = this.getFormattedScript(difference, false);
this.diffEditor.updateProperties({
contentLeft: sourceText,
contentRight: targetText,
title: diffEditorTitle
});
}
}));
this.tablelistenersToDispose.push(this.differencesTable.onCellAction(async (rowState) => {
let checkboxState = <azdata.ICheckboxCellActionEventArgs>rowState;
if (checkboxState) {
let diff = this.comparisonResult.differences[checkboxState.row];
await service.schemaCompareIncludeExcludeNode(this.comparisonResult.operationId, diff, checkboxState.checked, azdata.TaskExecutionMode.execute);
this.saveExcludeState(checkboxState);
}
}));
}
// save state based on source name if present otherwise target name (parity with SSDT)
private saveExcludeState(rowState: azdata.ICheckboxCellActionEventArgs) {
if (rowState) {
let diff = this.comparisonResult.differences[rowState.row];
let key = (diff.sourceValue && diff.sourceValue.length > 0) ? this.createName(diff.sourceValue) : this.createName(diff.targetValue);
if (key) {
if (!this.sourceTargetSwitched) {
this.originalSourceExcludes.delete(key);
this.removeExcludeEntry(this.scmpSourceExcludes, key);
if (!rowState.checked) {
this.originalSourceExcludes.set(key, diff);
if (this.originalSourceExcludes.size === this.comparisonResult.differences.length) {
this.setButtonStatesForNoChanges(false);
}
}
else {
this.setButtonStatesForNoChanges(true);
}
}
else {
this.originalTargetExcludes.delete(key);
this.removeExcludeEntry(this.scmpTargetExcludes, key);
if (!rowState.checked) {
this.originalTargetExcludes.set(key, diff);
if (this.originalTargetExcludes.size === this.comparisonResult.differences.length) {
this.setButtonStatesForNoChanges(false);
}
else {
this.setButtonStatesForNoChanges(true);
}
}
}
}
}
}
private shouldDiffBeIncluded(diff: mssql.DiffEntry): boolean {
let key = (diff.sourceValue && diff.sourceValue.length > 0) ? this.createName(diff.sourceValue) : this.createName(diff.targetValue);
if (key) {
if (this.sourceTargetSwitched === true
&& (this.originalTargetExcludes.has(key) || this.hasExcludeEntry(this.scmpTargetExcludes, key))) {
this.originalTargetExcludes.set(key, diff);
return false;
}
if (this.sourceTargetSwitched === false
&& (this.originalSourceExcludes.has(key) || this.hasExcludeEntry(this.scmpSourceExcludes, key))) {
this.originalSourceExcludes.set(key, diff);
return false;
}
return true;
}
return true;
}
private hasExcludeEntry(collection: mssql.SchemaCompareObjectId[], entryName: string): boolean {
let found = false;
if (collection) {
const index = collection.findIndex(e => this.createName(e.nameParts) === entryName);
found = index !== -1;
}
return found;
}
private removeExcludeEntry(collection: mssql.SchemaCompareObjectId[], entryName: string) {
if (collection) {
console.error('removing ' + entryName);
const index = collection.findIndex(e => this.createName(e.nameParts) === entryName);
collection.splice(index, 1);
}
}
private getAllDifferences(differences: mssql.DiffEntry[]): string[][] {
let data = [];
let finalDifferences: mssql.DiffEntry[] = [];
if (differences) {
differences.forEach(difference => {
if (difference.differenceType === mssql.SchemaDifferenceType.Object) {
if ((difference.sourceValue !== null && difference.sourceValue.length > 0) || (difference.targetValue !== null && difference.targetValue.length > 0)) {
finalDifferences.push(difference); // Add only non-null changes to ensure index does not mismatch between dictionay and UI - #6234
let state: boolean = this.shouldDiffBeIncluded(difference);
data.push([difference.name, this.createName(difference.sourceValue), state, this.SchemaCompareActionMap[difference.updateAction], this.createName(difference.targetValue)]);
}
}
});
}
this.comparisonResult.differences = finalDifferences;
return data;
}
private createName(nameParts: string[]): string {
if (isNullOrUndefined(nameParts) || nameParts.length === 0) {
return '';
}
return nameParts.join('.');
}
private getFormattedScript(diffEntry: mssql.DiffEntry, getSourceScript: boolean): string {
// if there is no entry, the script has to be \n because an empty string shows up as a difference but \n doesn't
if ((getSourceScript && diffEntry.sourceScript === null)
|| (!getSourceScript && diffEntry.targetScript === null)) {
return '\n';
}
let script = this.getAggregatedScript(diffEntry, getSourceScript);
return script;
}
private getAggregatedScript(diffEntry: mssql.DiffEntry, getSourceScript: boolean): string {
let script = '';
if (diffEntry !== null) {
let diffEntryScript = getSourceScript ? diffEntry.sourceScript : diffEntry.targetScript;
if (diffEntryScript) {
// add a blank line between each statement
script += diffEntryScript + '\n\n';
}
diffEntry.children.forEach(child => {
let childScript = this.getAggregatedScript(child, getSourceScript);
script += childScript;
});
}
return script;
}
public startCompare(): void {
this.flexModel.removeItem(this.splitView);
this.flexModel.removeItem(this.noDifferencesLabel);
this.flexModel.removeItem(this.startText);
this.flexModel.addItem(this.loader, { CSSStyles: { 'margin-top': '30px' } });
this.flexModel.addItem(this.waitText, { CSSStyles: { 'margin-top': '30px', 'align-self': 'center' } });
this.diffEditor.updateProperties({
contentLeft: os.EOL,
contentRight: os.EOL,
title: diffEditorTitle
});
this.differencesTable.selectedRows = null;
if (this.tablelistenersToDispose) {
this.tablelistenersToDispose.forEach(x => x.dispose());
}
this.resetButtons(ResetButtonState.comparing);
this.execute();
}
private createCompareButton(view: azdata.ModelView): void {
this.compareButton = view.modelBuilder.button().withProperties({
label: localize('schemaCompare.compareButton', 'Compare'),
iconPath: {
light: path.join(this.extensionContext.extensionPath, 'media', 'compare.svg'),
dark: path.join(this.extensionContext.extensionPath, 'media', 'compare-inverse.svg')
},
title: localize('schemaCompare.compareButtonTitle', 'Compare')
}).component();
this.compareButton.onDidClick(async (click) => {
this.startCompare();
});
}
private createCancelButton(view: azdata.ModelView): void {
this.cancelCompareButton = view.modelBuilder.button().withProperties({
label: localize('schemaCompare.cancelCompareButton', 'Stop'),
iconPath: {
light: path.join(this.extensionContext.extensionPath, 'media', 'stop.svg'),
dark: path.join(this.extensionContext.extensionPath, 'media', 'stop-inverse.svg')
},
title: localize('schemaCompare.cancelCompareButtonTitle', 'Stop')
}).component();
this.cancelCompareButton.onDidClick(async (click) => {
await this.cancelCompare();
});
}
private async cancelCompare() {
Telemetry.sendTelemetryEvent('SchemaCompareCancelStarted', {
'startTime:': Date.now().toString(),
'operationId': this.operationId
});
// clean the pane
this.flexModel.removeItem(this.loader);
this.flexModel.removeItem(this.waitText);
this.flexModel.addItem(this.startText, { CSSStyles: { 'margin': 'auto' } });
this.resetButtons(ResetButtonState.beforeCompareStart);
// cancel compare
if (this.operationId) {
const service = await this.getService();
const result = await service.schemaCompareCancel(this.operationId);
if (!result || !result.success) {
Telemetry.sendTelemetryEvent('SchemaCompareCancelFailed', {
'errorType': getTelemetryErrorType(result.errorMessage),
'operationId': this.operationId
});
vscode.window.showErrorMessage(
localize('schemaCompare.cancelErrorMessage', "Cancel schema compare failed: '{0}'", (result && result.errorMessage) ? result.errorMessage : 'Unknown'));
}
Telemetry.sendTelemetryEvent('SchemaCompareCancelEnded', {
'endTime:': Date.now().toString(),
'operationId': this.operationId
});
}
}
private createGenerateScriptButton(view: azdata.ModelView): void {
this.generateScriptButton = view.modelBuilder.button().withProperties({
label: localize('schemaCompare.generateScriptButton', 'Generate script'),
iconPath: {
light: path.join(this.extensionContext.extensionPath, 'media', 'generate-script.svg'),
dark: path.join(this.extensionContext.extensionPath, 'media', 'generate-script-inverse.svg')
},
}).component();
this.generateScriptButton.onDidClick(async (click) => {
Telemetry.sendTelemetryEvent('SchemaCompareGenerateScriptStarted', {
'startTime:': Date.now().toString(),
'operationId': this.comparisonResult.operationId
});
const service = await this.getService();
const result = await service.schemaCompareGenerateScript(this.comparisonResult.operationId, this.targetEndpointInfo.serverName, this.targetEndpointInfo.databaseName, azdata.TaskExecutionMode.script);
if (!result || !result.success) {
Telemetry.sendTelemetryEvent('SchemaCompareGenerateScriptFailed', {
'errorType': getTelemetryErrorType(result.errorMessage),
'operationId': this.comparisonResult.operationId
});
vscode.window.showErrorMessage(
localize('schemaCompare.generateScriptErrorMessage', "Generate script failed: '{0}'", (result && result.errorMessage) ? result.errorMessage : 'Unknown'));
}
Telemetry.sendTelemetryEvent('SchemaCompareGenerateScriptEnded', {
'endTime:': Date.now().toString(),
'operationId': this.comparisonResult.operationId
});
});
}
private createOptionsButton(view: azdata.ModelView) {
this.optionsButton = view.modelBuilder.button().withProperties({
label: localize('schemaCompare.optionsButton', 'Options'),
iconPath: {
light: path.join(this.extensionContext.extensionPath, 'media', 'options.svg'),
dark: path.join(this.extensionContext.extensionPath, 'media', 'options-inverse.svg')
},
title: localize('schemaCompare.optionsButtonTitle', 'Options')
}).component();
this.optionsButton.onDidClick(async (click) => {
Telemetry.sendTelemetryEvent('SchemaCompareOptionsOpened');
// create fresh every time
this.schemaCompareOptionDialog = new SchemaCompareOptionsDialog(this.deploymentOptions, this);
await this.schemaCompareOptionDialog.openDialog();
});
}
private createApplyButton(view: azdata.ModelView) {
this.applyButton = view.modelBuilder.button().withProperties({
label: localize('schemaCompare.updateButton', 'Apply'),
iconPath: {
light: path.join(this.extensionContext.extensionPath, 'media', 'start.svg'),
dark: path.join(this.extensionContext.extensionPath, 'media', 'start-inverse.svg')
},
}).component();
// need only yes button - since the modal dialog has a default cancel
const yesString = localize('schemaCompare.ApplyYes', 'Yes');
this.applyButton.onDidClick(async (click) => {
vscode.window.showWarningMessage(applyConfirmation, { modal: true }, yesString).then(async (result) => {
if (result === yesString) {
Telemetry.sendTelemetryEvent('SchemaCompareApplyStarted', {
'startTime': Date.now().toString(),
'operationId': this.comparisonResult.operationId
});
// disable apply and generate script buttons because the results are no longer valid after applying the changes
this.setButtonsForRecompare();
const service = await this.getService();
const result = await service.schemaComparePublishChanges(this.comparisonResult.operationId, this.targetEndpointInfo.serverName, this.targetEndpointInfo.databaseName, azdata.TaskExecutionMode.execute);
if (!result || !result.success) {
Telemetry.sendTelemetryEvent('SchemaCompareApplyFailed', {
'errorType': getTelemetryErrorType(result.errorMessage),
'operationId': this.comparisonResult.operationId
});
vscode.window.showErrorMessage(
localize('schemaCompare.updateErrorMessage', "Schema Compare Apply failed '{0}'", result.errorMessage ? result.errorMessage : 'Unknown'));
// reenable generate script and apply buttons if apply failed
this.generateScriptButton.enabled = true;
this.generateScriptButton.title = generateScriptEnabledMessage;
this.applyButton.enabled = true;
this.applyButton.title = applyEnabledMessage;
}
Telemetry.sendTelemetryEvent('SchemaCompareApplyEnded', {
'endTime': Date.now().toString(),
'operationId': this.comparisonResult.operationId
});
}
});
});
}
private resetButtons(resetButtonState: ResetButtonState): void {
switch (resetButtonState) {
case (ResetButtonState.noSourceTarget): {
this.compareButton.enabled = false;
this.optionsButton.enabled = false;
this.switchButton.enabled = this.sourceEndpointInfo ? true : false; // allows switching if the source is set
this.openScmpButton.enabled = true;
this.cancelCompareButton.enabled = false;
this.selectSourceButton.enabled = true;
this.selectTargetButton.enabled = true;
break;
}
// Before start and after complete are same functionally. Adding two enum values for clarity.
case (ResetButtonState.beforeCompareStart):
case (ResetButtonState.afterCompareComplete): {
this.compareButton.enabled = true;
this.optionsButton.enabled = true;
this.switchButton.enabled = true;
this.openScmpButton.enabled = true;
this.saveScmpButton.enabled = true;
this.cancelCompareButton.enabled = false;
this.selectSourceButton.enabled = true;
this.selectTargetButton.enabled = true;
break;
}
case (ResetButtonState.comparing): {
this.compareButton.enabled = false;
this.optionsButton.enabled = false;
this.switchButton.enabled = false;
this.openScmpButton.enabled = false;
this.cancelCompareButton.enabled = true;
this.selectSourceButton.enabled = false;
this.selectTargetButton.enabled = false;
break;
}
}
// Set generate script and apply to false because specific values depend on result and are set separately
this.generateScriptButton.enabled = false;
this.applyButton.enabled = false;
this.generateScriptButton.title = generateScriptEnabledMessage;
this.applyButton.title = applyEnabledMessage;
}
public setButtonsForRecompare(): void {
this.generateScriptButton.enabled = false;
this.applyButton.enabled = false;
this.generateScriptButton.title = reCompareToRefeshMessage;
this.applyButton.title = reCompareToRefeshMessage;
}
// reset state afer loading an scmp
private resetForNewCompare(): void {
this.resetButtons(ResetButtonState.beforeCompareStart);
this.flexModel.removeItem(this.splitView);
this.flexModel.removeItem(this.noDifferencesLabel);
this.flexModel.addItem(this.startText, { CSSStyles: { 'margin': 'auto' } });
}
private createSwitchButton(view: azdata.ModelView): void {
this.switchButton = view.modelBuilder.button().withProperties({
label: localize('schemaCompare.switchDirectionButton', 'Switch direction'),
iconPath: {
light: path.join(this.extensionContext.extensionPath, 'media', 'switch-directions.svg'),
dark: path.join(this.extensionContext.extensionPath, 'media', 'switch-directions-inverse.svg')
},
title: localize('schemaCompare.switchButtonTitle', 'Switch source and target')
}).component();
this.switchButton.onDidClick(async (click) => {
Telemetry.sendTelemetryEvent('SchemaCompareSwitch');
// switch source and target
[this.sourceEndpointInfo, this.targetEndpointInfo] = [this.targetEndpointInfo, this.sourceEndpointInfo];
[this.sourceName, this.targetName] = [this.targetName, this.sourceName];
this.sourceNameComponent.updateProperties({
columns: [
{
value: this.sourceName,
headerCssClass: 'no-borders',
toolTip: this.sourceName
},
]
});
this.targetNameComponent.updateProperties({
columns: [
{
value: this.targetName,
headerCssClass: 'no-borders',
toolTip: this.targetName
},
]
});
// remember that source target have been toggled
this.sourceTargetSwitched = this.sourceTargetSwitched ? false : true;
// only compare if both source and target are set
if (this.sourceEndpointInfo && this.targetEndpointInfo) {
this.startCompare();
}
});
}
private createSourceAndTargetButtons(view: azdata.ModelView): void {
this.selectSourceButton = view.modelBuilder.button().withProperties({
label: '•••',
title: localize('schemaCompare.sourceButtonTitle', "Select Source"),
ariaLabel: localize('schemaCompare.sourceButtonTitle', "Select Source")
}).component();
this.selectSourceButton.onDidClick(() => {
Telemetry.sendTelemetryEvent('SchemaCompareSelectSource');
let dialog = new SchemaCompareDialog(this);
dialog.openDialog();
});
this.selectTargetButton = view.modelBuilder.button().withProperties({
label: '•••',
title: localize('schemaCompare.targetButtonTitle', "Select Target"),
ariaLabel: localize('schemaCompare.targetButtonTitle', "Select Target")
}).component();
this.selectTargetButton.onDidClick(() => {
Telemetry.sendTelemetryEvent('SchemaCompareSelectTarget');
let dialog = new SchemaCompareDialog(this);
dialog.openDialog();
});
}
private createOpenScmpButton(view: azdata.ModelView) {
this.openScmpButton = view.modelBuilder.button().withProperties({
label: localize('schemaCompare.openScmpButton', 'Open .scmp file'),
iconPath: {
light: path.join(this.extensionContext.extensionPath, 'media', 'open-scmp.svg'),
dark: path.join(this.extensionContext.extensionPath, 'media', 'open-scmp-inverse.svg')
},
title: localize('schemaCompare.openScmpButtonTitle', 'Load source, target, and options saved in an .scmp file')
}).component();
this.openScmpButton.onDidClick(async (click) => {
Telemetry.sendTelemetryEvent('SchemaCompareOpenScmpStarted');
const rootPath = getRootPath();
let fileUris = await vscode.window.showOpenDialog(
{
canSelectFiles: true,
canSelectFolders: false,
canSelectMany: false,
defaultUri: vscode.Uri.file(rootPath),
openLabel: localize('schemaCompare.openFile', 'Open'),
filters: {
'scmp Files': ['scmp'],
}
}
);
if (!fileUris || fileUris.length === 0) {
return;
}
let fileUri = fileUris[0];
const service = await this.getService();
let startTime = Date.now();
const result = await service.schemaCompareOpenScmp(fileUri.fsPath);
if (!result || !result.success) {
Telemetry.sendTelemetryEvent('SchemaCompareOpenScmpFailed', {
'errorType': getTelemetryErrorType(result.errorMessage)
});
vscode.window.showErrorMessage(
localize('schemaCompare.openScmpErrorMessage', "Open scmp failed: '{0}'", (result && result.errorMessage) ? result.errorMessage : 'Unknown'));
return;
}
if (result.sourceEndpointInfo && result.sourceEndpointInfo.endpointType === mssql.SchemaCompareEndpointType.Database) {
// only set endpoint info if able to connect to the database
const ownerUri = await verifyConnectionAndGetOwnerUri(result.sourceEndpointInfo);
if (ownerUri) {
this.sourceEndpointInfo = result.sourceEndpointInfo;
this.sourceEndpointInfo.ownerUri = ownerUri;
}
} else {
// need to do this instead of just setting it to the result.sourceEndpointInfo because some fields are null which will cause an error when sending the compare request
this.sourceEndpointInfo = {
endpointType: mssql.SchemaCompareEndpointType.Dacpac,
serverDisplayName: '',
serverName: '',
databaseName: '',
ownerUri: '',
packageFilePath: result.sourceEndpointInfo.packageFilePath,
connectionDetails: undefined
};
}
if (result.targetEndpointInfo && result.targetEndpointInfo.endpointType === mssql.SchemaCompareEndpointType.Database) {
const ownerUri = await verifyConnectionAndGetOwnerUri(result.targetEndpointInfo);
if (ownerUri) {
this.targetEndpointInfo = result.targetEndpointInfo;
this.targetEndpointInfo.ownerUri = ownerUri;
}
} else {
// need to do this instead of just setting it to the result.targetEndpointInfo because some fields are null which will cause an error when sending the compare request
this.targetEndpointInfo = {
endpointType: mssql.SchemaCompareEndpointType.Dacpac,
serverDisplayName: '',
serverName: '',
databaseName: '',
ownerUri: '',
packageFilePath: result.targetEndpointInfo.packageFilePath,
connectionDetails: undefined
};
}
this.updateSourceAndTarget();
this.setDeploymentOptions(result.deploymentOptions);
this.scmpSourceExcludes = result.excludedSourceElements;
this.scmpTargetExcludes = result.excludedTargetElements;
this.sourceTargetSwitched = result.originalTargetName !== this.targetEndpointInfo.databaseName;
// clear out any old results
this.resetForNewCompare();
Telemetry.sendTelemetryEvent('SchemaCompareOpenScmpEnded', {
'elapsedTime:': (Date.now() - startTime).toString()
});
});
}
private createSaveScmpButton(view: azdata.ModelView): void {
this.saveScmpButton = view.modelBuilder.button().withProperties({
label: localize('schemaCompare.saveScmpButton', 'Save .scmp file'),
iconPath: {
light: path.join(this.extensionContext.extensionPath, 'media', 'save-scmp.svg'),
dark: path.join(this.extensionContext.extensionPath, 'media', 'save-scmp-inverse.svg')
},
title: localize('schemaCompare.saveScmpButtonTitle', 'Save source and target, options, and excluded elements'),
enabled: false
}).component();
this.saveScmpButton.onDidClick(async (click) => {
const rootPath = getRootPath();
const filePath = await vscode.window.showSaveDialog(
{
defaultUri: vscode.Uri.file(rootPath),
saveLabel: localize('schemaCompare.saveFile', 'Save'),
filters: {
'scmp Files': ['scmp'],
}
}
);
if (!filePath) {
return;
}
// convert include/exclude maps to arrays of object ids
let sourceExcludes: mssql.SchemaCompareObjectId[] = this.convertExcludesToObjectIds(this.originalSourceExcludes);
let targetExcludes: mssql.SchemaCompareObjectId[] = this.convertExcludesToObjectIds(this.originalTargetExcludes);
let startTime = Date.now();
Telemetry.sendTelemetryEvent('SchemaCompareSaveScmp');
const service = await this.getService();
const result = await service.schemaCompareSaveScmp(this.sourceEndpointInfo, this.targetEndpointInfo, azdata.TaskExecutionMode.execute, this.deploymentOptions, filePath.fsPath, sourceExcludes, targetExcludes);
if (!result || !result.success) {
Telemetry.sendTelemetryEvent('SchemaCompareSaveScmpFailed', {
'errorType': getTelemetryErrorType(result.errorMessage),
'operationId': this.comparisonResult.operationId
});
vscode.window.showErrorMessage(
localize('schemaCompare.saveScmpErrorMessage', "Save scmp failed: '{0}'", (result && result.errorMessage) ? result.errorMessage : 'Unknown'));
}
Telemetry.sendTelemetryEvent('SchemaCompareSaveScmpEnded', {
'elapsedTime:': (Date.now() - startTime).toString(),
'operationId': this.comparisonResult.operationId
});
});
}
/**
* Converts excluded diff entries into object ids which are needed to save them in an scmp
*/
private convertExcludesToObjectIds(excludedDiffEntries: Map<string, mssql.DiffEntry>): mssql.SchemaCompareObjectId[] {
let result = [];
excludedDiffEntries.forEach((value: mssql.DiffEntry) => {
result.push({
nameParts: value.sourceValue ? value.sourceValue : value.targetValue,
sqlObjectType: `Microsoft.Data.Tools.Schema.Sql.SchemaModel.${value.name}`
});
});
return result;
}
private setButtonStatesForNoChanges(enableButtons: boolean): void {
// generate script and apply can only be enabled if the target is a database
if (this.targetEndpointInfo.endpointType === mssql.SchemaCompareEndpointType.Database) {
this.applyButton.enabled = enableButtons;
this.generateScriptButton.enabled = enableButtons;
this.applyButton.title = enableButtons ? applyEnabledMessage : applyNoChangesMessage;
this.generateScriptButton.title = enableButtons ? generateScriptEnabledMessage : generateScriptNoChangesMessage;
}
}
private async getService(): Promise<mssql.ISchemaCompareService> {
if (isNullOrUndefined(this.schemaCompareService)) {
this.schemaCompareService = (vscode.extensions.getExtension(mssql.extension.name).exports as mssql.IExtension).schemaCompare;
}
return this.schemaCompareService;
}
private async GetDefaultDeploymentOptions(): Promise<void> {
// Same as dacfx default options
const service = await this.getService();
let result = await service.schemaCompareGetDefaultOptions();
this.setDeploymentOptions(result.defaultDeploymentOptions);
}
}
// Borrowed as is from other extensions
// TODO : figure out if any inbuilt alternative is available
export function generateGuid(): string {
let hexValues: string[] = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'];
// c.f. rfc4122 (UUID version 4 = xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx)
let oct: string = '';
let tmp: number;
/* tslint:disable:no-bitwise */
for (let a: number = 0; a < 4; a++) {
tmp = (4294967296 * Math.random()) | 0;
oct += hexValues[tmp & 0xF] +
hexValues[tmp >> 4 & 0xF] +
hexValues[tmp >> 8 & 0xF] +
hexValues[tmp >> 12 & 0xF] +
hexValues[tmp >> 16 & 0xF] +
hexValues[tmp >> 20 & 0xF] +
hexValues[tmp >> 24 & 0xF] +
hexValues[tmp >> 28 & 0xF];
}
// 'Set the two most significant bits (bits 6 and 7) of the clock_seq_hi_and_reserved to zero and one, respectively'
let clockSequenceHi: string = hexValues[8 + (Math.random() * 4) | 0];
return oct.substr(0, 8) + '-' + oct.substr(9, 4) + '-4' + oct.substr(13, 3) + '-' + clockSequenceHi + oct.substr(16, 3) + '-' + oct.substr(19, 12);
/* tslint:enable:no-bitwise */
}