Schema Compare extension (#4974)
* extension now working * fix diff editor title disappearing and remove border from source and target name boxes * redoing a bunch of stuff that disappeared after rebasing * add images and add to extensions.ts * moving a few changes to the right place after rebase * formatting * update toolbar svgs * addressing comments * add return types * Adding PR comments * Adding light and dark theme icons * Fixing the diff editor title for dark theme
@@ -433,4 +433,27 @@ export namespace AddServerGroupRequest {
|
||||
export namespace RemoveServerGroupRequest {
|
||||
export const type = new RequestType<RemoveServerGroupParams, boolean, void, void>('cms/removeCmsServerGroup');
|
||||
}
|
||||
// ------------------------------- <CMS> ----------------------------------------
|
||||
// ------------------------------- <CMS> ----------------------------------------
|
||||
|
||||
// ------------------------------- <Schema Compare> -----------------------------
|
||||
export interface SchemaCompareParams {
|
||||
sourceEndpointInfo: azdata.SchemaCompareEndpointInfo;
|
||||
targetEndpointInfo: azdata.SchemaCompareEndpointInfo;
|
||||
taskExecutionMode: TaskExecutionMode;
|
||||
}
|
||||
|
||||
export interface SchemaCompareGenerateScriptParams {
|
||||
operationId: string;
|
||||
targetDatabaseName: string;
|
||||
scriptFilePath: string;
|
||||
taskExecutionMode: TaskExecutionMode;
|
||||
}
|
||||
|
||||
export namespace SchemaCompareRequest {
|
||||
export const type = new RequestType<SchemaCompareParams, azdata.SchemaCompareResult, void, void>('schemaCompare/compare');
|
||||
}
|
||||
|
||||
export namespace SchemaCompareGenerateScriptRequest {
|
||||
export const type = new RequestType<SchemaCompareGenerateScriptParams, azdata.ResultStatus, void, void>('schemaCompare/generateScript');
|
||||
}
|
||||
// ------------------------------- <Schema Compare> -----------------------------
|
||||
|
||||
@@ -145,6 +145,64 @@ export class DacFxServicesFeature extends SqlOpsFeature<undefined> {
|
||||
}
|
||||
}
|
||||
|
||||
export class SchemaCompareServicesFeature extends SqlOpsFeature<undefined> {
|
||||
private static readonly messageTypes: RPCMessageType[] = [
|
||||
contracts.SchemaCompareRequest.type,
|
||||
contracts.SchemaCompareGenerateScriptRequest.type
|
||||
];
|
||||
|
||||
constructor(client: SqlOpsDataClient) {
|
||||
super(client, SchemaCompareServicesFeature.messageTypes);
|
||||
}
|
||||
|
||||
public fillClientCapabilities(capabilities: ClientCapabilities): void {
|
||||
}
|
||||
|
||||
public initialize(capabilities: ServerCapabilities): void {
|
||||
this.register(this.messages, {
|
||||
id: UUID.generateUuid(),
|
||||
registerOptions: undefined
|
||||
});
|
||||
}
|
||||
|
||||
protected registerProvider(options: undefined): Disposable {
|
||||
const client = this._client;
|
||||
let self = this;
|
||||
|
||||
let schemaCompare = (sourceEndpointInfo: azdata.SchemaCompareEndpointInfo, targetEndpointInfo: azdata.SchemaCompareEndpointInfo, taskExecutionMode: azdata.TaskExecutionMode): Thenable<azdata.SchemaCompareResult> => {
|
||||
let params: contracts.SchemaCompareParams = {sourceEndpointInfo: sourceEndpointInfo, targetEndpointInfo: targetEndpointInfo, taskExecutionMode: taskExecutionMode};
|
||||
return client.sendRequest(contracts.SchemaCompareRequest.type, params).then(
|
||||
r => {
|
||||
return r;
|
||||
},
|
||||
e => {
|
||||
client.logFailedRequest(contracts.SchemaCompareRequest.type, e);
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
let schemaCompareGenerateScript = (operationId: string, targetDatabaseName: string, scriptFilePath: string, taskExecutionMode: azdata.TaskExecutionMode): Thenable<azdata.DacFxResult> => {
|
||||
let params: contracts.SchemaCompareGenerateScriptParams = {operationId: operationId, targetDatabaseName: targetDatabaseName, scriptFilePath: scriptFilePath, taskExecutionMode: taskExecutionMode};
|
||||
return client.sendRequest(contracts.SchemaCompareGenerateScriptRequest.type, params).then(
|
||||
r => {
|
||||
return r;
|
||||
},
|
||||
e => {
|
||||
client.logFailedRequest(contracts.SchemaCompareGenerateScriptRequest.type, e);
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
return azdata.dataprotocol.registerSchemaCompareServicesProvider({
|
||||
providerId: client.providerId,
|
||||
schemaCompare,
|
||||
schemaCompareGenerateScript
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class AgentServicesFeature extends SqlOpsFeature<undefined> {
|
||||
private static readonly messagesTypes: RPCMessageType[] = [
|
||||
contracts.AgentJobsRequest.type,
|
||||
|
||||
@@ -21,7 +21,7 @@ import { CredentialStore } from './credentialstore/credentialstore';
|
||||
import { AzureResourceProvider } from './resourceProvider/resourceProvider';
|
||||
import * as Utils from './utils';
|
||||
import { Telemetry, LanguageClientErrorHandler } from './telemetry';
|
||||
import { TelemetryFeature, AgentServicesFeature, DacFxServicesFeature } from './features';
|
||||
import { TelemetryFeature, AgentServicesFeature, DacFxServicesFeature, SchemaCompareServicesFeature } from './features';
|
||||
import { AppContext } from './appContext';
|
||||
import { ApiWrapper } from './apiWrapper';
|
||||
import { UploadFilesCommand, MkDirCommand, SaveFileCommand, PreviewFileCommand, CopyPathCommand, DeleteFilesCommand } from './objectExplorerNodeProvider/hdfsCommands';
|
||||
@@ -77,6 +77,7 @@ export async function activate(context: vscode.ExtensionContext): Promise<MssqlE
|
||||
TelemetryFeature,
|
||||
AgentServicesFeature,
|
||||
DacFxServicesFeature,
|
||||
SchemaCompareServicesFeature
|
||||
],
|
||||
outputChannel: new CustomOutputChannel()
|
||||
};
|
||||
|
||||
29
extensions/schema-compare/README.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# Microsoft SQL Server Schema Compare for Azure Data Studio
|
||||
|
||||
Microsoft SQL Server Schema Compare for Azure Data Studio includes:
|
||||
|
||||
## Schema Compare *(preview)*
|
||||
The Schema Compare extension provides an easy to use experience to compare .dacpac files and databases and apply the changes from source to target.
|
||||
|
||||
This experience is currently in its initial preview. Please report issues and feature requests [here.](https://github.com/microsoft/azuredatastudio/issues)
|
||||
|
||||
### How do I start a Schema Comparison?
|
||||
* The main entry point for schema compare is to right click a database in the Object Explorer, and click **Schema Compare**.
|
||||
* The user can also launch the schema compare dialog from the command palette (Ctrl+Shift+P) by searching for **Schema Compare**
|
||||
|
||||
### Why would I use the Schema Compare?
|
||||
Schema Compare was created to add the ability to compare the schemas from .dacpac files and databases and apply the changes.
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
|
||||
|
||||
## Privacy Statement
|
||||
|
||||
The [Microsoft Enterprise and Developer Privacy Statement](https://privacy.microsoft.com/en-us/privacystatement) describes the privacy statement of this software.
|
||||
|
||||
## License
|
||||
|
||||
Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
|
||||
Licensed under the [Source EULA](https://raw.githubusercontent.com/Microsoft/azuredatastudio/master/LICENSE.txt).
|
||||
1
extensions/schema-compare/images/dark_icon.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 21 21"><defs><style>.cls-1{fill:#fff;}.cls-2{fill:#231f20;}.cls-3{fill:#0095d7;}</style></defs><title>importflatfile_inverse</title><path class="cls-1" d="M13.34,1.57c-2.81,0-7.52.53-7.65,2.49v3.2L7,8.54V5.66a17.11,17.11,0,0,0,6.37,1,17.1,17.1,0,0,0,6.38-1V17.58c-.17.46-2.55,1.35-6.38,1.35a19.63,19.63,0,0,1-3.43-.27V20a23.78,23.78,0,0,0,3.43.25c2.86,0,7.66-.57,7.66-2.64V4.06C20.87,2.1,16.16,1.57,13.34,1.57Zm6.38,2.55c-.2.45-2.56,1.28-6.38,1.28S7.24,4.6,7,4.14c.27-.47,2.6-1.29,6.37-1.29s6.16.85,6.38,1.25h0Z"/><polygon class="cls-2" points="18.55 3.06 18.53 3.07 18.53 3.04 18.55 3.06"/><path class="cls-1" d="M7,10,5.69,8.68,5,8H0V19.85H8.91v-8ZM5.2,9.24l.49.49L7,11l.67.67H5.2Zm3,9.86H.74V8.71H4.46v3.71H8.17Z"/><path class="cls-3" d="M16.5,15a.27.27,0,0,1-.08.2L14.2,17.4a.26.26,0,0,1-.19.08.28.28,0,0,1-.2-.08.26.26,0,0,1-.08-.2.82.82,0,0,1,0-.14l.06-.25.08-.32.08-.32.07-.27,0-.17H4.5v-1.5h9.59l0-.17L14,13.79l-.08-.32-.08-.31-.06-.25a.91.91,0,0,1,0-.14.26.26,0,0,1,.08-.2.28.28,0,0,1,.2-.08.26.26,0,0,1,.19.08l2.22,2.22A.26.26,0,0,1,16.5,15Z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
1
extensions/schema-compare/images/light_icon.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 21 21"><defs><style>.cls-1{fill:#212121;}.cls-2{fill:#231f20;}.cls-3{fill:#00539c;}</style></defs><title>importflatfile</title><path class="cls-1" d="M13.34,1.3c-2.81,0-7.52.53-7.65,2.49V7L7,8.27V5.39a17.11,17.11,0,0,0,6.37,1,17.1,17.1,0,0,0,6.38-1V17.31c-.17.46-2.55,1.35-6.38,1.35a19.63,19.63,0,0,1-3.43-.27V19.7a23.78,23.78,0,0,0,3.43.25C16.2,20,21,19.38,21,17.31V3.79C20.87,1.83,16.16,1.3,13.34,1.3Zm6.38,2.55c-.2.45-2.56,1.28-6.38,1.28S7.24,4.33,7,3.87c.27-.47,2.6-1.29,6.37-1.29s6.16.85,6.38,1.25h0Z"/><polygon class="cls-2" points="18.55 2.79 18.53 2.81 18.53 2.78 18.55 2.79"/><path class="cls-1" d="M7,9.69,5.69,8.41,5,7.7H0V19.58H8.91v-8ZM5.2,9l.49.49L7,10.74l.67.67H5.2Zm3,9.86H.74V8.44H4.46v3.71H8.17Z"/><path class="cls-3" d="M16.5,14.72a.27.27,0,0,1-.08.2L14.2,17.14a.26.26,0,0,1-.19.08.28.28,0,0,1-.2-.08.26.26,0,0,1-.08-.2.82.82,0,0,1,0-.14l.06-.25.08-.32.08-.32.07-.27,0-.17H4.5V14h9.59l0-.17L14,13.53l-.08-.32-.08-.31-.06-.25a.91.91,0,0,1,0-.14.26.26,0,0,1,.08-.2.28.28,0,0,1,.2-.08.26.26,0,0,1,.19.08l2.22,2.22A.26.26,0,0,1,16.5,14.72Z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
BIN
extensions/schema-compare/images/sqlserver.png
Normal file
|
After Width: | Height: | Size: 37 KiB |
52
extensions/schema-compare/package.json
Normal file
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"name": "schema-compare",
|
||||
"displayName": "SQL Server Schema Compare",
|
||||
"description": "SQL Server Schema Compare for Azure Data Studio supports comparing the schemas of databases and dacpacs.",
|
||||
"version": "0.1.0",
|
||||
"publisher": "Microsoft",
|
||||
"preview": true,
|
||||
"engines": {
|
||||
"vscode": "^1.25.0",
|
||||
"sqlops": "*"
|
||||
},
|
||||
"license": "https://raw.githubusercontent.com/Microsoft/azuredatastudio/master/extensions/import/Microsoft_SQL_Server_Import_Extension_and_Tools_Import_Flat_File_Preview.docx",
|
||||
"icon": "images/sqlserver.png",
|
||||
"aiKey": "AIF-5574968e-856d-40d2-af67-c89a14e76412",
|
||||
"activationEvents": [
|
||||
"*"
|
||||
],
|
||||
"main": "./out/main",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/Microsoft/azuredatastudio.git"
|
||||
},
|
||||
"extensionDependencies": [
|
||||
"Microsoft.mssql"
|
||||
],
|
||||
"contributes": {
|
||||
"commands": [
|
||||
{
|
||||
"command": "schemaCompare.start",
|
||||
"title": "Schema Compare",
|
||||
"icon": {
|
||||
"light": "./images/light_icon.svg",
|
||||
"dark": "./images/dark_icon.svg"
|
||||
}
|
||||
}
|
||||
],
|
||||
"menus": {
|
||||
"objectExplorer/item/context": [
|
||||
{
|
||||
"command": "schemaCompare.start",
|
||||
"when": "connectionProvider == MSSQL && nodeType && nodeType == Database",
|
||||
"group": "export"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"vscode-extension-telemetry": "0.0.18",
|
||||
"vscode-nls": "^3.2.1"
|
||||
},
|
||||
"devDependencies": {}
|
||||
}
|
||||
41
extensions/schema-compare/src/controllers/mainController.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import * as azdata from 'azdata';
|
||||
import * as vscode from 'vscode';
|
||||
import { SchemaCompareDialog } from '../dialogs/schemaCompareDialog';
|
||||
|
||||
/**
|
||||
* The main controller class that initializes the extension
|
||||
*/
|
||||
export default class MainController implements vscode.Disposable {
|
||||
protected _context: vscode.ExtensionContext;
|
||||
|
||||
public constructor(context: vscode.ExtensionContext) {
|
||||
this._context = context;
|
||||
}
|
||||
|
||||
public get extensionContext(): vscode.ExtensionContext {
|
||||
return this._context;
|
||||
}
|
||||
|
||||
public deactivate(): void {
|
||||
}
|
||||
|
||||
public activate(): Promise<boolean> {
|
||||
this.initializeSchemaCompareDialog();
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
private initializeSchemaCompareDialog(): void {
|
||||
azdata.tasks.registerTask('schemaCompare.start', (profile: azdata.IConnectionProfile) => new SchemaCompareDialog().openDialog(profile));
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this.deactivate();
|
||||
}
|
||||
}
|
||||
462
extensions/schema-compare/src/dialogs/schemaCompareDialog.ts
Normal file
@@ -0,0 +1,462 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import * as nls from 'vscode-nls';
|
||||
import * as azdata from 'azdata';
|
||||
import * as vscode from 'vscode';
|
||||
import * as os from 'os';
|
||||
import { SchemaCompareResult } from '../schemaCompareResult';
|
||||
|
||||
const localize = nls.loadMessageBundle();
|
||||
const CompareButtonText: string = localize('schemaCompareDialog.Compare', 'Compare');
|
||||
const CancelButtonText: string = localize('schemaCompareDialog.Cancel', 'Cancel');
|
||||
const SourceTextBoxLabel: string = localize('schemaCompareDialog.SourceLabel', 'Source File');
|
||||
const TargetTextBoxLabel: string = localize('schemaCompareDialog.TargetLabel', 'Target File');
|
||||
const DacpacRadioButtonLabel: string = localize('schemaCompare.dacpacRadioButtonLabel', 'Data-tier Application File (.dacpac)');
|
||||
const DatabaseRadioButtonLabel: string = localize('schemaCompare.databaseButtonLabel', 'Database');
|
||||
const SourceRadioButtonsLabel: string = localize('schemaCompare.sourceButtonsLabel', 'Source Type');
|
||||
const TargetRadioButtonsLabel: string = localize('schemaCompare.targetButtonsLabel', 'Target Type');
|
||||
const NoActiveConnectionsLabel: string = localize('schemaCompare.NoActiveConnectionsText', 'No active connections');
|
||||
const SchemaCompareLabel: string = localize('schemaCompare.dialogTitle', 'Schema Compare');
|
||||
|
||||
export class SchemaCompareDialog {
|
||||
public dialog: azdata.window.Dialog;
|
||||
private schemaCompareTab: azdata.window.DialogTab;
|
||||
private sourceDacpacComponent: azdata.FormComponent;
|
||||
private sourceTextBox: azdata.InputBoxComponent;
|
||||
private sourceFileButton: azdata.ButtonComponent;
|
||||
private sourceServerComponent: azdata.FormComponent;
|
||||
private sourceServerDropdown: azdata.DropDownComponent;
|
||||
private sourceDatabaseComponent: azdata.FormComponent;
|
||||
private sourceDatabaseDropdown: azdata.DropDownComponent;
|
||||
private sourceNoActiveConnectionsText: azdata.FormComponent;
|
||||
private targetDacpacComponent: azdata.FormComponent;
|
||||
private targetTextBox: azdata.InputBoxComponent;
|
||||
private targetFileButton: azdata.ButtonComponent;
|
||||
private targetServerComponent: azdata.FormComponent;
|
||||
private targetServerDropdown: azdata.DropDownComponent;
|
||||
private targetDatabaseComponent: azdata.FormComponent;
|
||||
private targetDatabaseDropdown: azdata.DropDownComponent;
|
||||
private targetNoActiveConnectionsText: azdata.FormComponent;
|
||||
private formBuilder: azdata.FormBuilder;
|
||||
private sourceIsDacpac: boolean;
|
||||
private targetIsDacpac: boolean;
|
||||
private database: string;
|
||||
public dialogName: string;
|
||||
|
||||
protected initializeDialog(): void {
|
||||
this.schemaCompareTab = azdata.window.createTab(SchemaCompareLabel);
|
||||
this.initializeSchemaCompareTab();
|
||||
this.dialog.content = [this.schemaCompareTab];
|
||||
}
|
||||
|
||||
public openDialog(p: any, dialogName?: string): void {
|
||||
let profile = p ? <azdata.IConnectionProfile>p.connectionProfile : undefined;
|
||||
if (profile) {
|
||||
this.database = profile.databaseName;
|
||||
}
|
||||
|
||||
let event = dialogName ? dialogName : null;
|
||||
this.dialog = azdata.window.createModelViewDialog(SchemaCompareLabel, event);
|
||||
|
||||
this.initializeDialog();
|
||||
|
||||
this.dialog.okButton.label = CompareButtonText;
|
||||
this.dialog.okButton.onClick(async () => await this.execute());
|
||||
|
||||
this.dialog.cancelButton.label = CancelButtonText;
|
||||
this.dialog.cancelButton.onClick(async () => await this.cancel());
|
||||
|
||||
azdata.window.openDialog(this.dialog);
|
||||
}
|
||||
|
||||
protected async execute(): Promise<void> {
|
||||
let sourceName: string;
|
||||
let targetName: string;
|
||||
|
||||
let sourceEndpointInfo: azdata.SchemaCompareEndpointInfo;
|
||||
if (this.sourceIsDacpac) {
|
||||
sourceName = this.sourceTextBox.value;
|
||||
sourceEndpointInfo = {
|
||||
endpointType: azdata.SchemaCompareEndpointType.dacpac,
|
||||
databaseName: '',
|
||||
ownerUri: '',
|
||||
packageFilePath: this.sourceTextBox.value
|
||||
};
|
||||
} else {
|
||||
sourceName = (this.sourceServerDropdown.value as ConnectionDropdownValue).name + '.' + (<azdata.CategoryValue>this.sourceDatabaseDropdown.value).name;
|
||||
let ownerUri = await azdata.connection.getUriForConnection((this.sourceServerDropdown.value as ConnectionDropdownValue).connection.connectionId);
|
||||
|
||||
sourceEndpointInfo = {
|
||||
endpointType: azdata.SchemaCompareEndpointType.database,
|
||||
databaseName: (<azdata.CategoryValue>this.sourceDatabaseDropdown.value).name,
|
||||
ownerUri: ownerUri,
|
||||
packageFilePath: ''
|
||||
};
|
||||
}
|
||||
|
||||
let targetEndpointInfo: azdata.SchemaCompareEndpointInfo;
|
||||
if (this.targetIsDacpac) {
|
||||
targetName = this.targetTextBox.value;
|
||||
targetEndpointInfo = {
|
||||
endpointType: azdata.SchemaCompareEndpointType.dacpac,
|
||||
databaseName: '',
|
||||
ownerUri: '',
|
||||
packageFilePath: this.targetTextBox.value
|
||||
};
|
||||
} else {
|
||||
targetName = (this.targetServerDropdown.value as ConnectionDropdownValue).name + '.' + (<azdata.CategoryValue>this.targetDatabaseDropdown.value).name;
|
||||
let ownerUri = await azdata.connection.getUriForConnection((this.targetServerDropdown.value as ConnectionDropdownValue).connection.connectionId);
|
||||
|
||||
targetEndpointInfo = {
|
||||
endpointType: azdata.SchemaCompareEndpointType.database,
|
||||
databaseName: (<azdata.CategoryValue>this.targetDatabaseDropdown.value).name,
|
||||
ownerUri: ownerUri,
|
||||
packageFilePath: ''
|
||||
};
|
||||
}
|
||||
|
||||
let schemaCompareResult = new SchemaCompareResult(sourceName, targetName, sourceEndpointInfo, targetEndpointInfo);
|
||||
schemaCompareResult.start();
|
||||
}
|
||||
|
||||
protected async cancel(): Promise<void> {
|
||||
}
|
||||
|
||||
private initializeSchemaCompareTab(): void {
|
||||
this.schemaCompareTab.registerContent(async view => {
|
||||
this.sourceTextBox = view.modelBuilder.inputBox().withProperties({
|
||||
width: 275
|
||||
}).component();
|
||||
|
||||
this.targetTextBox = view.modelBuilder.inputBox().withProperties({
|
||||
width: 275
|
||||
}).component();
|
||||
|
||||
this.sourceServerComponent = await this.createSourceServerDropdown(view);
|
||||
await this.populateServerDropdown(false);
|
||||
|
||||
this.sourceDatabaseComponent = await this.createSourceDatabaseDropdown(view);
|
||||
if ((this.sourceServerDropdown.value as ConnectionDropdownValue)) {
|
||||
await this.populateDatabaseDropdown((this.sourceServerDropdown.value as ConnectionDropdownValue).connection.connectionId, false);
|
||||
}
|
||||
|
||||
this.targetServerComponent = await this.createTargetServerDropdown(view);
|
||||
await this.populateServerDropdown(true);
|
||||
|
||||
this.targetDatabaseComponent = await this.createTargetDatabaseDropdown(view);
|
||||
if ((this.targetServerDropdown.value as ConnectionDropdownValue)) {
|
||||
await this.populateDatabaseDropdown((this.targetServerDropdown.value as ConnectionDropdownValue).connection.connectionId, true);
|
||||
}
|
||||
|
||||
this.sourceDacpacComponent = await this.createFileBrowser(view, false);
|
||||
this.targetDacpacComponent = await this.createFileBrowser(view, true);
|
||||
|
||||
let sourceRadioButtons = await this.createSourceRadiobuttons(view);
|
||||
let targetRadioButtons = await this.createTargetRadiobuttons(view);
|
||||
|
||||
this.sourceNoActiveConnectionsText = await this.createNoActiveConnectionsText(view);
|
||||
this.targetNoActiveConnectionsText = await this.createNoActiveConnectionsText(view);
|
||||
|
||||
// if schema compare was launched from a db context menu, set that db as the source
|
||||
if (this.database) {
|
||||
this.formBuilder = view.modelBuilder.formContainer()
|
||||
.withFormItems([
|
||||
sourceRadioButtons,
|
||||
this.sourceServerComponent,
|
||||
this.sourceDatabaseComponent,
|
||||
targetRadioButtons,
|
||||
this.targetDacpacComponent
|
||||
], {
|
||||
horizontal: true
|
||||
});
|
||||
} else {
|
||||
this.formBuilder = view.modelBuilder.formContainer()
|
||||
.withFormItems([
|
||||
sourceRadioButtons,
|
||||
this.sourceDacpacComponent,
|
||||
targetRadioButtons,
|
||||
this.targetDacpacComponent
|
||||
], {
|
||||
horizontal: true
|
||||
});
|
||||
}
|
||||
let formModel = this.formBuilder.component();
|
||||
await view.initializeModel(formModel);
|
||||
});
|
||||
}
|
||||
|
||||
private async createFileBrowser(view: azdata.ModelView, isTarget: boolean): Promise<azdata.FormComponent> {
|
||||
let currentTextbox = isTarget ? this.targetTextBox : this.sourceTextBox;
|
||||
if (isTarget) {
|
||||
this.targetFileButton = view.modelBuilder.button().withProperties({
|
||||
label: '•••',
|
||||
}).component();
|
||||
} else {
|
||||
this.sourceFileButton = view.modelBuilder.button().withProperties({
|
||||
label: '•••',
|
||||
}).component();
|
||||
}
|
||||
|
||||
let currentButton = isTarget ? this.targetFileButton : this.sourceFileButton;
|
||||
|
||||
currentButton.onDidClick(async (click) => {
|
||||
let fileUris = await vscode.window.showOpenDialog(
|
||||
{
|
||||
canSelectFiles: true,
|
||||
canSelectFolders: false,
|
||||
canSelectMany: false,
|
||||
defaultUri: vscode.Uri.file(os.homedir()),
|
||||
openLabel: localize('schemaCompare.openFile', 'Open'),
|
||||
filters: {
|
||||
'dacpac Files': ['dacpac'],
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (!fileUris || fileUris.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let fileUri = fileUris[0];
|
||||
currentTextbox.value = fileUri.fsPath;
|
||||
});
|
||||
|
||||
return {
|
||||
component: currentTextbox,
|
||||
title: isTarget ? TargetTextBoxLabel : SourceTextBoxLabel,
|
||||
actions: [currentButton]
|
||||
};
|
||||
}
|
||||
|
||||
private async createSourceRadiobuttons(view: azdata.ModelView): Promise<azdata.FormComponent> {
|
||||
let dacpacRadioButton = view.modelBuilder.radioButton()
|
||||
.withProperties({
|
||||
name: 'source',
|
||||
label: DacpacRadioButtonLabel
|
||||
}).component();
|
||||
|
||||
let databaseRadioButton = view.modelBuilder.radioButton()
|
||||
.withProperties({
|
||||
name: 'source',
|
||||
label: DatabaseRadioButtonLabel
|
||||
}).component();
|
||||
|
||||
// show dacpac file browser
|
||||
dacpacRadioButton.onDidClick(() => {
|
||||
this.sourceIsDacpac = true;
|
||||
this.formBuilder.removeFormItem(this.sourceNoActiveConnectionsText);
|
||||
this.formBuilder.removeFormItem(this.sourceServerComponent);
|
||||
this.formBuilder.removeFormItem(this.sourceDatabaseComponent);
|
||||
this.formBuilder.insertFormItem(this.sourceDacpacComponent, 1, { horizontal: true });
|
||||
});
|
||||
|
||||
// show server and db dropdowns or 'No active connections' text
|
||||
databaseRadioButton.onDidClick(() => {
|
||||
this.sourceIsDacpac = false;
|
||||
if ((this.sourceServerDropdown.value as ConnectionDropdownValue)) {
|
||||
this.formBuilder.insertFormItem(this.sourceServerComponent, 1, { horizontal: true, componentWidth: 300 });
|
||||
this.formBuilder.insertFormItem(this.sourceDatabaseComponent, 2, { horizontal: true, componentWidth: 300 });
|
||||
} else {
|
||||
this.formBuilder.insertFormItem(this.sourceNoActiveConnectionsText, 1, { horizontal: true });
|
||||
}
|
||||
this.formBuilder.removeFormItem(this.sourceDacpacComponent);
|
||||
});
|
||||
|
||||
if (this.database) {
|
||||
databaseRadioButton.checked = true;
|
||||
this.sourceIsDacpac = false;
|
||||
} else {
|
||||
dacpacRadioButton.checked = true;
|
||||
this.sourceIsDacpac = true;
|
||||
}
|
||||
let flexRadioButtonsModel = view.modelBuilder.flexContainer()
|
||||
.withLayout({ flexFlow: 'column' })
|
||||
.withItems([dacpacRadioButton, databaseRadioButton]
|
||||
).component();
|
||||
|
||||
return {
|
||||
component: flexRadioButtonsModel,
|
||||
title: SourceRadioButtonsLabel
|
||||
};
|
||||
}
|
||||
|
||||
private async createTargetRadiobuttons(view: azdata.ModelView): Promise<azdata.FormComponent> {
|
||||
let dacpacRadioButton = view.modelBuilder.radioButton()
|
||||
.withProperties({
|
||||
name: 'target',
|
||||
label: DacpacRadioButtonLabel
|
||||
}).component();
|
||||
|
||||
let databaseRadioButton = view.modelBuilder.radioButton()
|
||||
.withProperties({
|
||||
name: 'target',
|
||||
label: DatabaseRadioButtonLabel
|
||||
}).component();
|
||||
|
||||
// show dacpac file browser
|
||||
dacpacRadioButton.onDidClick(() => {
|
||||
this.targetIsDacpac = true;
|
||||
this.formBuilder.removeFormItem(this.targetNoActiveConnectionsText);
|
||||
this.formBuilder.removeFormItem(this.targetServerComponent);
|
||||
this.formBuilder.removeFormItem(this.targetDatabaseComponent);
|
||||
this.formBuilder.addFormItem(this.targetDacpacComponent, { horizontal: true });
|
||||
});
|
||||
|
||||
// show server and db dropdowns or 'No active connections' text
|
||||
databaseRadioButton.onDidClick(() => {
|
||||
this.targetIsDacpac = false;
|
||||
this.formBuilder.removeFormItem(this.targetDacpacComponent);
|
||||
if ((this.targetServerDropdown.value as ConnectionDropdownValue)) {
|
||||
this.formBuilder.addFormItem(this.targetServerComponent, { horizontal: true, componentWidth: 300 });
|
||||
this.formBuilder.addFormItem(this.targetDatabaseComponent, { horizontal: true, componentWidth: 300 });
|
||||
} else {
|
||||
this.formBuilder.addFormItem(this.targetNoActiveConnectionsText, { horizontal: true });
|
||||
}
|
||||
});
|
||||
|
||||
dacpacRadioButton.checked = true;
|
||||
this.targetIsDacpac = true;
|
||||
let flexRadioButtonsModel = view.modelBuilder.flexContainer()
|
||||
.withLayout({ flexFlow: 'column' })
|
||||
.withItems([dacpacRadioButton, databaseRadioButton]
|
||||
).component();
|
||||
|
||||
return {
|
||||
component: flexRadioButtonsModel,
|
||||
title: TargetRadioButtonsLabel
|
||||
};
|
||||
}
|
||||
|
||||
protected async createSourceServerDropdown(view: azdata.ModelView): Promise<azdata.FormComponent> {
|
||||
this.sourceServerDropdown = view.modelBuilder.dropDown().component();
|
||||
this.sourceServerDropdown.onValueChanged(async () => {
|
||||
await this.populateDatabaseDropdown((this.sourceServerDropdown.value as ConnectionDropdownValue).connection.connectionId, false);
|
||||
});
|
||||
|
||||
return {
|
||||
component: this.sourceServerDropdown,
|
||||
title: localize('schemaCompare.sourceServerDropdownTitle', 'Source Server')
|
||||
};
|
||||
}
|
||||
|
||||
protected async createTargetServerDropdown(view: azdata.ModelView): Promise<azdata.FormComponent> {
|
||||
this.targetServerDropdown = view.modelBuilder.dropDown().component();
|
||||
this.targetServerDropdown.onValueChanged(async () => {
|
||||
await this.populateDatabaseDropdown((this.targetServerDropdown.value as ConnectionDropdownValue).connection.connectionId, true);
|
||||
});
|
||||
|
||||
return {
|
||||
component: this.targetServerDropdown,
|
||||
title: localize('schemaCompare.targetServerDropdownTitle', 'Target Server')
|
||||
};
|
||||
}
|
||||
|
||||
protected async populateServerDropdown(isTarget: boolean): Promise<void> {
|
||||
let currentDropdown = isTarget ? this.targetServerDropdown : this.sourceServerDropdown;
|
||||
let values = await this.getServerValues();
|
||||
|
||||
currentDropdown.updateProperties({
|
||||
values: values
|
||||
});
|
||||
}
|
||||
|
||||
protected async getServerValues(): Promise<{ connection: azdata.connection.Connection, displayName: string, name: string }[]> {
|
||||
let cons = await azdata.connection.getActiveConnections();
|
||||
// This user has no active connections
|
||||
if (!cons || cons.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let values = cons.map(c => {
|
||||
let db = c.options.databaseDisplayName;
|
||||
let usr = c.options.user;
|
||||
let srv = c.options.server;
|
||||
|
||||
if (!db) {
|
||||
db = '<default>';
|
||||
}
|
||||
|
||||
if (!usr) {
|
||||
usr = 'default';
|
||||
}
|
||||
|
||||
let finalName = `${srv}, ${db} (${usr})`;
|
||||
return {
|
||||
connection: c,
|
||||
displayName: finalName,
|
||||
name: srv
|
||||
};
|
||||
});
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
protected async createSourceDatabaseDropdown(view: azdata.ModelView): Promise<azdata.FormComponent> {
|
||||
this.sourceDatabaseDropdown = view.modelBuilder.dropDown().component();
|
||||
|
||||
return {
|
||||
component: this.sourceDatabaseDropdown,
|
||||
title: localize('schemaCompare.sourceDatabaseDropdownTitle', 'Source Database')
|
||||
};
|
||||
}
|
||||
|
||||
protected async createTargetDatabaseDropdown(view: azdata.ModelView): Promise<azdata.FormComponent> {
|
||||
this.targetDatabaseDropdown = view.modelBuilder.dropDown().component();
|
||||
|
||||
return {
|
||||
component: this.targetDatabaseDropdown,
|
||||
title: localize('schemaCompare.targetDatabaseDropdownTitle', 'Target Database')
|
||||
};
|
||||
}
|
||||
|
||||
protected async populateDatabaseDropdown(connectionId: string, isTarget: boolean): Promise<void> {
|
||||
let currentDropdown = isTarget ? this.targetDatabaseDropdown : this.sourceDatabaseDropdown;
|
||||
currentDropdown.updateProperties({ values: [] });
|
||||
|
||||
let values = await this.getDatabaseValues(connectionId);
|
||||
currentDropdown.updateProperties({
|
||||
values: values
|
||||
});
|
||||
}
|
||||
|
||||
protected async getDatabaseValues(connectionId: string): Promise<{ displayName, name }[]> {
|
||||
let idx = -1;
|
||||
let count = -1;
|
||||
let values = (await azdata.connection.listDatabases(connectionId)).map(db => {
|
||||
count++;
|
||||
// if schema compare was launched from a db context menu, set that db at the top of the dropdown
|
||||
if (this.database && db === this.database) {
|
||||
idx = count;
|
||||
}
|
||||
|
||||
return {
|
||||
displayName: db,
|
||||
name: db
|
||||
};
|
||||
});
|
||||
|
||||
if (idx >= 0) {
|
||||
let tmp = values[0];
|
||||
values[0] = values[idx];
|
||||
values[idx] = tmp;
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
protected async createNoActiveConnectionsText(view: azdata.ModelView): Promise<azdata.FormComponent> {
|
||||
let noActiveConnectionsText = view.modelBuilder.text().withProperties({ value: NoActiveConnectionsLabel }).component();
|
||||
|
||||
return {
|
||||
component: noActiveConnectionsText,
|
||||
title: ''
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
interface ConnectionDropdownValue extends azdata.CategoryValue {
|
||||
connection: azdata.connection.Connection;
|
||||
}
|
||||
24
extensions/schema-compare/src/main.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
import * as vscode from 'vscode';
|
||||
import MainController from './controllers/mainController';
|
||||
|
||||
let controllers: MainController[] = [];
|
||||
|
||||
export async function activate(context: vscode.ExtensionContext): Promise<void> {
|
||||
// Start the main controller
|
||||
let mainController = new MainController(context);
|
||||
controllers.push(mainController);
|
||||
context.subscriptions.push(mainController);
|
||||
|
||||
await mainController.activate();
|
||||
}
|
||||
|
||||
export function deactivate(): void {
|
||||
for (let controller of controllers) {
|
||||
controller.deactivate();
|
||||
}
|
||||
}
|
||||
10
extensions/schema-compare/src/media/compare-inverse.svg
Normal file
@@ -0,0 +1,10 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0)">
|
||||
<path d="M13 8C13.4115 8 13.7995 8.08073 14.1641 8.24219C14.5286 8.39844 14.8464 8.61198 15.1172 8.88281C15.388 9.15365 15.6016 9.47135 15.7578 9.83594C15.9193 10.2005 16 10.5885 16 11C16 11.4115 15.9193 11.7995 15.7578 12.1641C15.6016 12.5286 15.388 12.8464 15.1172 13.1172C14.8464 13.388 14.5286 13.6042 14.1641 13.7656C13.7995 13.9219 13.4115 14 13 14C12.6875 14 12.3828 13.9531 12.0859 13.8594C11.7891 13.7656 11.5156 13.6276 11.2656 13.4453C11.1823 13.5286 11.0417 13.6745 10.8438 13.8828C10.651 14.0859 10.4271 14.3203 10.1719 14.5859C9.91667 14.8464 9.64583 15.1198 9.35938 15.4062C9.07812 15.6875 8.8125 15.9479 8.5625 16.1875C8.3125 16.4219 8.09115 16.6172 7.89844 16.7734C7.70573 16.9245 7.57292 17 7.5 17C7.36458 17 7.2474 16.9505 7.14844 16.8516C7.04948 16.7526 7 16.6354 7 16.5C7 16.4271 7.07552 16.2943 7.22656 16.1016C7.38281 15.9089 7.57812 15.6875 7.8125 15.4375C8.05208 15.1875 8.3125 14.9219 8.59375 14.6406C8.88021 14.3542 9.15365 14.0833 9.41406 13.8281C9.67969 13.5729 9.91406 13.349 10.1172 13.1562C10.3255 12.9583 10.4714 12.8177 10.5547 12.7344C10.3724 12.4844 10.2344 12.2109 10.1406 11.9141C10.0469 11.6172 10 11.3125 10 11C10 10.5885 10.0781 10.2005 10.2344 9.83594C10.3958 9.47135 10.612 9.15365 10.8828 8.88281C11.1536 8.61198 11.4714 8.39844 11.8359 8.24219C12.2005 8.08073 12.5885 8 13 8ZM13 13C13.2708 13 13.5286 12.9479 13.7734 12.8438C14.0182 12.7344 14.2292 12.5911 14.4062 12.4141C14.5885 12.2318 14.7318 12.0208 14.8359 11.7812C14.9453 11.5365 15 11.276 15 11C15 10.7292 14.9453 10.4714 14.8359 10.2266C14.7318 9.98177 14.5885 9.77083 14.4062 9.59375C14.2292 9.41146 14.0182 9.26823 13.7734 9.16406C13.5286 9.05469 13.2708 9 13 9C12.724 9 12.4635 9.05469 12.2188 9.16406C11.9792 9.26823 11.7682 9.41146 11.5859 9.59375C11.4089 9.77083 11.2656 9.98177 11.1562 10.2266C11.0521 10.4714 11 10.7292 11 11C11 11.276 11.0521 11.5365 11.1562 11.7812C11.2656 12.0208 11.4089 12.2318 11.5859 12.4141C11.7682 12.5911 11.9792 12.7344 12.2188 12.8438C12.4635 12.9479 12.724 13 13 13ZM2 12V10H0V1H12V3H14V7.14062C13.8333 7.09896 13.6667 7.0651 13.5 7.03906C13.3333 7.01302 13.1667 7 13 7V4H3V11H9C9 11.1667 9.01302 11.3333 9.03906 11.5C9.0651 11.6667 9.09896 11.8333 9.14062 12H2ZM2 9V3H11V2H1V9H2Z" fill="#4894FE"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0">
|
||||
<rect width="16" height="16" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
10
extensions/schema-compare/src/media/compare.svg
Normal file
@@ -0,0 +1,10 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0)">
|
||||
<path d="M13 8C13.4115 8 13.7995 8.08073 14.1641 8.24219C14.5286 8.39844 14.8464 8.61198 15.1172 8.88281C15.388 9.15365 15.6016 9.47135 15.7578 9.83594C15.9193 10.2005 16 10.5885 16 11C16 11.4115 15.9193 11.7995 15.7578 12.1641C15.6016 12.5286 15.388 12.8464 15.1172 13.1172C14.8464 13.388 14.5286 13.6042 14.1641 13.7656C13.7995 13.9219 13.4115 14 13 14C12.6875 14 12.3828 13.9531 12.0859 13.8594C11.7891 13.7656 11.5156 13.6276 11.2656 13.4453C11.1823 13.5286 11.0417 13.6745 10.8438 13.8828C10.651 14.0859 10.4271 14.3203 10.1719 14.5859C9.91667 14.8464 9.64583 15.1198 9.35938 15.4062C9.07812 15.6875 8.8125 15.9479 8.5625 16.1875C8.3125 16.4219 8.09115 16.6172 7.89844 16.7734C7.70573 16.9245 7.57292 17 7.5 17C7.36458 17 7.2474 16.9505 7.14844 16.8516C7.04948 16.7526 7 16.6354 7 16.5C7 16.4271 7.07552 16.2943 7.22656 16.1016C7.38281 15.9089 7.57812 15.6875 7.8125 15.4375C8.05208 15.1875 8.3125 14.9219 8.59375 14.6406C8.88021 14.3542 9.15365 14.0833 9.41406 13.8281C9.67969 13.5729 9.91406 13.349 10.1172 13.1562C10.3255 12.9583 10.4714 12.8177 10.5547 12.7344C10.3724 12.4844 10.2344 12.2109 10.1406 11.9141C10.0469 11.6172 10 11.3125 10 11C10 10.5885 10.0781 10.2005 10.2344 9.83594C10.3958 9.47135 10.612 9.15365 10.8828 8.88281C11.1536 8.61198 11.4714 8.39844 11.8359 8.24219C12.2005 8.08073 12.5885 8 13 8ZM13 13C13.2708 13 13.5286 12.9479 13.7734 12.8438C14.0182 12.7344 14.2292 12.5911 14.4062 12.4141C14.5885 12.2318 14.7318 12.0208 14.8359 11.7812C14.9453 11.5365 15 11.276 15 11C15 10.7292 14.9453 10.4714 14.8359 10.2266C14.7318 9.98177 14.5885 9.77083 14.4062 9.59375C14.2292 9.41146 14.0182 9.26823 13.7734 9.16406C13.5286 9.05469 13.2708 9 13 9C12.724 9 12.4635 9.05469 12.2188 9.16406C11.9792 9.26823 11.7682 9.41146 11.5859 9.59375C11.4089 9.77083 11.2656 9.98177 11.1562 10.2266C11.0521 10.4714 11 10.7292 11 11C11 11.276 11.0521 11.5365 11.1562 11.7812C11.2656 12.0208 11.4089 12.2318 11.5859 12.4141C11.7682 12.5911 11.9792 12.7344 12.2188 12.8438C12.4635 12.9479 12.724 13 13 13ZM2 12V10H0V1H12V3H14V7.14062C13.8333 7.09896 13.6667 7.0651 13.5 7.03906C13.3333 7.01302 13.1667 7 13 7V4H3V11H9C9 11.1667 9.01302 11.3333 9.03906 11.5C9.0651 11.6667 9.09896 11.8333 9.14062 12H2ZM2 9V3H11V2H1V9H2Z" fill="#015CDA"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0">
|
||||
<rect width="16" height="16" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14 4.28906V16H1V0H9.71094L14 4.28906ZM10 4H12.2891L10 1.71094V4ZM13 15V5H9V1H2V15H13ZM5.85156 7.35156L4.20312 9L5.85156 10.6484L5.14844 11.3516L2.79688 9L5.14844 6.64844L5.85156 7.35156ZM9.85156 6.64844L12.2031 9L9.85156 11.3516L9.14844 10.6484L10.7969 9L9.14844 7.35156L9.85156 6.64844Z" fill="#4894FE"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 419 B |
3
extensions/schema-compare/src/media/generate-script.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14 4.28906V16H1V0H9.71094L14 4.28906ZM10 4H12.2891L10 1.71094V4ZM13 15V5H9V1H2V15H13ZM5.85156 7.35156L4.20312 9L5.85156 10.6484L5.14844 11.3516L2.79688 9L5.14844 6.64844L5.85156 7.35156ZM9.85156 6.64844L12.2031 9L9.85156 11.3516L9.14844 10.6484L10.7969 9L9.14844 7.35156L9.85156 6.64844Z" fill="#015CDA"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 419 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M16 11V12H1.95312L3.22656 13.2734L2.52344 13.9766L0.046875 11.5L2.52344 9.02344L3.22656 9.72656L1.95312 11H16ZM12.7734 6.27344L14.0469 5H0V4H14.0469L12.7734 2.72656L13.4766 2.02344L15.9531 4.5L13.4766 6.97656L12.7734 6.27344Z" fill="#4894FE"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 356 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M16 11V12H1.95312L3.22656 13.2734L2.52344 13.9766L0.046875 11.5L2.52344 9.02344L3.22656 9.72656L1.95312 11H16ZM12.7734 6.27344L14.0469 5H0V4H14.0469L12.7734 2.72656L13.4766 2.02344L15.9531 4.5L13.4766 6.97656L12.7734 6.27344Z" fill="#015CDA"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 356 B |
366
extensions/schema-compare/src/schemaCompareResult.ts
Normal file
@@ -0,0 +1,366 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
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';
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
export class SchemaCompareResult {
|
||||
private differencesTable: azdata.TableComponent;
|
||||
private loader: azdata.LoadingComponent;
|
||||
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 generateScriptButton: azdata.ButtonComponent;
|
||||
private SchemaCompareActionMap: Map<Number, string>;
|
||||
private comparisonResult: azdata.SchemaCompareResult;
|
||||
private sourceNameComponent: azdata.TableComponent;
|
||||
private targetNameComponent: azdata.TableComponent;
|
||||
|
||||
constructor(private sourceName: string, private targetName: string, private sourceEndpointInfo: azdata.SchemaCompareEndpointInfo, private targetEndpointInfo: azdata.SchemaCompareEndpointInfo) {
|
||||
this.SchemaCompareActionMap = new Map<Number, string>();
|
||||
this.SchemaCompareActionMap[azdata.SchemaUpdateAction.Delete] = localize('schemaCompare.deleteAction', 'Delete');
|
||||
this.SchemaCompareActionMap[azdata.SchemaUpdateAction.Change] = localize('schemaCompare.changeAction', 'Change');
|
||||
this.SchemaCompareActionMap[azdata.SchemaUpdateAction.Add] = localize('schemaCompare.addAction', 'Add');
|
||||
|
||||
this.editor = azdata.workspace.createModelViewEditor(localize('schemaCompare.Title', 'Schema Compare'), { retainContextWhenHidden: true, supportsSave: true });
|
||||
|
||||
this.editor.registerContent(async view => {
|
||||
this.differencesTable = view.modelBuilder.table().withProperties({
|
||||
data: [],
|
||||
height: 300,
|
||||
}).component();
|
||||
|
||||
this.diffEditor = view.modelBuilder.diffeditor().withProperties({
|
||||
contentLeft: os.EOL,
|
||||
contentRight: os.EOL,
|
||||
height: 500,
|
||||
title: localize('schemaCompare.ObjectDefinitionsTitle', 'Object Definitions')
|
||||
}).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.createGenerateScriptButton(view);
|
||||
this.resetButtons();
|
||||
|
||||
let toolBar = view.modelBuilder.toolbarContainer();
|
||||
toolBar.addToolbarItems([{
|
||||
component: this.compareButton
|
||||
}, {
|
||||
component: this.generateScriptButton,
|
||||
toolbarSeparatorAfter: true
|
||||
},
|
||||
{
|
||||
component: this.switchButton
|
||||
}]);
|
||||
|
||||
let sourceLabel = view.modelBuilder.text().withProperties({
|
||||
value: localize('schemaCompare.sourceLabel', 'Source')
|
||||
}).component();
|
||||
|
||||
let targetLabel = view.modelBuilder.text().withProperties({
|
||||
value: localize('schemaCompare.targetLabel', 'Target')
|
||||
}).component();
|
||||
|
||||
let arrowLabel = view.modelBuilder.text().withProperties({
|
||||
value: localize('schemaCompare.switchLabel', '➔')
|
||||
}).component();
|
||||
|
||||
this.sourceNameComponent = view.modelBuilder.table().withProperties({
|
||||
columns: [
|
||||
{
|
||||
value: sourceName,
|
||||
headerCssClass: 'no-borders',
|
||||
toolTip: sourceName
|
||||
},
|
||||
]
|
||||
}).component();
|
||||
|
||||
this.targetNameComponent = view.modelBuilder.table().withProperties({
|
||||
columns: [
|
||||
{
|
||||
value: targetName,
|
||||
headerCssClass: 'no-borders',
|
||||
toolTip: 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': '45%', 'height': '25px', 'margin-top': '10px', 'margin-left': '15px' } });
|
||||
this.sourceTargetFlexLayout.addItem(arrowLabel, { CSSStyles: { 'width': '10%', 'font-size': 'larger', 'text-align-last': 'center' } });
|
||||
this.sourceTargetFlexLayout.addItem(this.targetNameComponent, { CSSStyles: { 'width': '45%', 'height': '25px', 'margin-top': '10px', 'margin-left': '15px' } });
|
||||
|
||||
this.loader = view.modelBuilder.loadingComponent().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.loader, { CSSStyles: { 'margin-top': '30px' } });
|
||||
this.flexModel.setLayout({
|
||||
flexFlow: 'column',
|
||||
height: '100%'
|
||||
});
|
||||
|
||||
await view.initializeModel(this.flexModel);
|
||||
});
|
||||
}
|
||||
|
||||
public start(): void {
|
||||
this.editor.openEditor();
|
||||
this.execute();
|
||||
}
|
||||
|
||||
private async execute(): Promise<void> {
|
||||
let service = await SchemaCompareResult.getService('MSSQL');
|
||||
this.comparisonResult = await service.schemaCompare(this.sourceEndpointInfo, this.targetEndpointInfo, azdata.TaskExecutionMode.execute);
|
||||
if (!this.comparisonResult || !this.comparisonResult.success) {
|
||||
vscode.window.showErrorMessage(localize('schemaCompare.compareErrorMessage', "Schema Compare failed: {0}", this.comparisonResult.errorMessage ? this.comparisonResult.errorMessage : 'Unknown'));
|
||||
return;
|
||||
}
|
||||
|
||||
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', 'Target Name'),
|
||||
cssClass: 'align-with-header',
|
||||
width: 90
|
||||
},
|
||||
{
|
||||
value: localize('schemaCompare.actionColumn', 'Action'),
|
||||
cssClass: 'align-with-header',
|
||||
width: 30
|
||||
},
|
||||
{
|
||||
value: localize('schemaCompare.targetNameColumn', 'Source Name'),
|
||||
cssClass: 'align-with-header',
|
||||
width: 150
|
||||
}]
|
||||
});
|
||||
|
||||
this.splitView.addItem(this.differencesTable);
|
||||
this.splitView.addItem(this.diffEditor);
|
||||
this.splitView.setLayout({
|
||||
orientation: 'vertical',
|
||||
splitViewHeight: 800
|
||||
});
|
||||
|
||||
this.flexModel.removeItem(this.loader);
|
||||
this.switchButton.enabled = true;
|
||||
this.compareButton.enabled = true;
|
||||
|
||||
if (this.comparisonResult.differences.length > 0) {
|
||||
this.flexModel.addItem(this.splitView);
|
||||
|
||||
// only enable generate script button if the target is a db
|
||||
if (this.targetEndpointInfo.endpointType === azdata.SchemaCompareEndpointType.database) {
|
||||
this.generateScriptButton.enabled = true;
|
||||
} else {
|
||||
this.generateScriptButton.title = localize('schemaCompare.generateScriptButtonDisabledTitle', 'Generate script is enabled when the target is a database');
|
||||
}
|
||||
} else {
|
||||
this.flexModel.addItem(this.noDifferencesLabel, { CSSStyles: { 'margin': 'auto' } });
|
||||
}
|
||||
|
||||
let sourceText = '';
|
||||
let targetText = '';
|
||||
this.differencesTable.onRowSelected(() => {
|
||||
let difference = this.comparisonResult.differences[this.differencesTable.selectedRows[0]];
|
||||
if (difference !== undefined) {
|
||||
sourceText = difference.sourceScript === null ? '\n' : this.getAggregatedScript(difference, true);
|
||||
targetText = difference.targetScript === null ? '\n' : this.getAggregatedScript(difference, false);
|
||||
|
||||
this.diffEditor.updateProperties({
|
||||
contentLeft: sourceText,
|
||||
contentRight: targetText,
|
||||
title: localize('schemaCompare.ObjectDefinitionsTitle', 'Object Definitions')
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private getAllDifferences(differences: azdata.DiffEntry[]): string[][] {
|
||||
let data = [];
|
||||
if (differences) {
|
||||
differences.forEach(difference => {
|
||||
if (difference.differenceType === azdata.SchemaDifferenceType.Object) {
|
||||
if (difference.sourceValue !== null || difference.targetValue !== null) {
|
||||
data.push([difference.name, difference.sourceValue, this.SchemaCompareActionMap[difference.updateAction], difference.targetValue]);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
private getAggregatedScript(diffEntry: azdata.DiffEntry, getSourceScript: boolean): string {
|
||||
let script = '';
|
||||
if (diffEntry !== null) {
|
||||
script += getSourceScript ? diffEntry.sourceScript : diffEntry.targetScript;
|
||||
diffEntry.children.forEach(child => {
|
||||
let childScript = this.getAggregatedScript(child, getSourceScript);
|
||||
if (childScript !== 'null') {
|
||||
script += childScript;
|
||||
}
|
||||
});
|
||||
}
|
||||
return script;
|
||||
}
|
||||
|
||||
private reExecute(): void {
|
||||
this.flexModel.removeItem(this.splitView);
|
||||
this.flexModel.removeItem(this.noDifferencesLabel);
|
||||
this.flexModel.addItem(this.loader, { CSSStyles: { 'margin-top': '30px' } });
|
||||
this.diffEditor.updateProperties({
|
||||
contentLeft: os.EOL,
|
||||
contentRight: os.EOL
|
||||
});
|
||||
this.differencesTable.selectedRows = null;
|
||||
this.resetButtons();
|
||||
this.execute();
|
||||
}
|
||||
|
||||
private createCompareButton(view: azdata.ModelView): void {
|
||||
this.compareButton = view.modelBuilder.button().withProperties({
|
||||
label: localize('schemaCompare.compareButton', 'Compare'),
|
||||
iconPath: {
|
||||
light: path.join(__dirname, 'media', 'compare.svg'),
|
||||
dark: path.join(__dirname, 'media', 'compare-inverse.svg')
|
||||
},
|
||||
title: localize('schemaCompare.compareButtonTitle', 'Compare')
|
||||
}).component();
|
||||
|
||||
this.compareButton.onDidClick(async (click) => {
|
||||
this.reExecute();
|
||||
});
|
||||
}
|
||||
|
||||
private createGenerateScriptButton(view: azdata.ModelView): void {
|
||||
this.generateScriptButton = view.modelBuilder.button().withProperties({
|
||||
label: localize('schemaCompare.generateScriptButton', 'Generate script'),
|
||||
iconPath: {
|
||||
light : path.join(__dirname, 'media', 'generate-script.svg'),
|
||||
dark: path.join(__dirname, 'media', 'generate-script-inverse.svg')
|
||||
},
|
||||
}).component();
|
||||
|
||||
this.generateScriptButton.onDidClick(async (click) => {
|
||||
// get file path
|
||||
let now = new Date();
|
||||
let datetime = now.getFullYear() + '-' + (now.getMonth() + 1) + '-' + now.getDate() + '-' + now.getHours() + '-' + now.getMinutes() + '-' + now.getSeconds();
|
||||
let defaultFilePath = path.join(os.homedir(), this.targetName + '_Update_' + datetime + '.sql');
|
||||
let fileUri = await vscode.window.showSaveDialog(
|
||||
{
|
||||
defaultUri: vscode.Uri.file(defaultFilePath),
|
||||
saveLabel: localize('schemaCompare.saveFile', 'Save'),
|
||||
filters: {
|
||||
'SQL Files': ['sql'],
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (!fileUri) {
|
||||
return;
|
||||
}
|
||||
|
||||
let service = await SchemaCompareResult.getService('MSSQL');
|
||||
let result = await service.schemaCompareGenerateScript(this.comparisonResult.operationId, this.targetEndpointInfo.databaseName, fileUri.fsPath, azdata.TaskExecutionMode.execute);
|
||||
if (!result || !result.success) {
|
||||
vscode.window.showErrorMessage(
|
||||
localize('schemaCompare.generateScriptErrorMessage', "Generate script failed: '{0}'", (result && result.errorMessage) ? result.errorMessage : 'Unknown'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private resetButtons(): void {
|
||||
this.compareButton.enabled = false;
|
||||
this.switchButton.enabled = false;
|
||||
this.generateScriptButton.enabled = false;
|
||||
this.generateScriptButton.title = localize('schemaCompare.generateScriptEnabledButton', 'Generate script to deploy changes to target');
|
||||
}
|
||||
|
||||
private createSwitchButton(view: azdata.ModelView): void {
|
||||
let swapIcon = path.join(__dirname, 'media', 'switch-directions.svg');
|
||||
|
||||
this.switchButton = view.modelBuilder.button().withProperties({
|
||||
label: localize('schemaCompare.switchDirectionButton', 'Switch direction'),
|
||||
iconPath: {
|
||||
light : path.join(__dirname, 'media', 'switch-directions.svg'),
|
||||
dark: path.join(__dirname, 'media', 'switch-directions-inverse.svg')
|
||||
},
|
||||
title: localize('schemaCompare.switchButtonTitle', 'Switch source and target')
|
||||
}).component();
|
||||
|
||||
this.switchButton.onDidClick(async (click) => {
|
||||
// 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
|
||||
},
|
||||
]
|
||||
});
|
||||
|
||||
this.reExecute();
|
||||
});
|
||||
}
|
||||
|
||||
private static async getService(providerName: string): Promise<azdata.SchemaCompareServicesProvider> {
|
||||
let service = azdata.dataprotocol.getProvider<azdata.SchemaCompareServicesProvider>(providerName, azdata.DataProviderType.SchemaCompareServicesProvider);
|
||||
return service;
|
||||
}
|
||||
}
|
||||
9
extensions/schema-compare/src/typings/ref.d.ts
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
/// <reference path='../../../../src/vs/vscode.d.ts'/>
|
||||
/// <reference path='../../../../src/sql/azdata.d.ts'/>
|
||||
/// <reference path='../../../../src/sql/azdata.proposed.d.ts'/>
|
||||
/// <reference types='@types/node'/>
|
||||
20
extensions/schema-compare/tsconfig.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"compileOnSave": true,
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "es6",
|
||||
"outDir": "./out",
|
||||
"lib": [
|
||||
"es6",
|
||||
"es2015.promise"
|
||||
],
|
||||
"sourceMap": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"moduleResolution": "node",
|
||||
"declaration": false
|
||||
},
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
46
extensions/schema-compare/yarn.lock
Normal file
@@ -0,0 +1,46 @@
|
||||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
applicationinsights@1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/applicationinsights/-/applicationinsights-1.0.1.tgz#53446b830fe8d5d619eee2a278b31d3d25030927"
|
||||
integrity sha1-U0Rrgw/o1dYZ7uKieLMdPSUDCSc=
|
||||
dependencies:
|
||||
diagnostic-channel "0.2.0"
|
||||
diagnostic-channel-publishers "0.2.1"
|
||||
zone.js "0.7.6"
|
||||
|
||||
diagnostic-channel-publishers@0.2.1:
|
||||
version "0.2.1"
|
||||
resolved "https://registry.yarnpkg.com/diagnostic-channel-publishers/-/diagnostic-channel-publishers-0.2.1.tgz#8e2d607a8b6d79fe880b548bc58cc6beb288c4f3"
|
||||
integrity sha1-ji1geottef6IC1SLxYzGvrKIxPM=
|
||||
|
||||
diagnostic-channel@0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/diagnostic-channel/-/diagnostic-channel-0.2.0.tgz#cc99af9612c23fb1fff13612c72f2cbfaa8d5a17"
|
||||
integrity sha1-zJmvlhLCP7H/8TYSxy8sv6qNWhc=
|
||||
dependencies:
|
||||
semver "^5.3.0"
|
||||
|
||||
semver@^5.3.0:
|
||||
version "5.7.0"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.0.tgz#790a7cf6fea5459bac96110b29b60412dc8ff96b"
|
||||
integrity sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==
|
||||
|
||||
vscode-extension-telemetry@0.0.18:
|
||||
version "0.0.18"
|
||||
resolved "https://registry.yarnpkg.com/vscode-extension-telemetry/-/vscode-extension-telemetry-0.0.18.tgz#602ba20d8c71453aa34533a291e7638f6e5c0327"
|
||||
integrity sha512-Vw3Sr+dZwl+c6PlsUwrTtCOJkgrmvS3OUVDQGcmpXWAgq9xGq6as0K4pUx+aGqTjzLAESmWSrs6HlJm6J6Khcg==
|
||||
dependencies:
|
||||
applicationinsights "1.0.1"
|
||||
|
||||
vscode-nls@^3.2.1:
|
||||
version "3.2.5"
|
||||
resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-3.2.5.tgz#25520c1955108036dec607c85e00a522f247f1a4"
|
||||
integrity sha512-ITtoh3V4AkWXMmp3TB97vsMaHRgHhsSFPsUdzlueSL+dRZbSNTZeOmdQv60kjCV306ghPxhDeoNUEm3+EZMuyw==
|
||||
|
||||
zone.js@0.7.6:
|
||||
version "0.7.6"
|
||||
resolved "https://registry.yarnpkg.com/zone.js/-/zone.js-0.7.6.tgz#fbbc39d3e0261d0986f1ba06306eb3aeb0d22009"
|
||||
integrity sha1-+7w50+AmHQmG8boGMG6zrrDSIAk=
|
||||