mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-01-13 17:22:15 -05:00
Apply changes from remote database to sqlproj - sql-database-projects changes (#17738)
* update project from database * update project from database * Leftover merge update * Slight refactor to add vscode entrypoints * Re-adding leftover schemacompare bits that reference database project changes * Removing unnecessary function * Addiung GetDSP command to package.json * tests and a race condition fix * remove custom UUID generation code * swapping awaits for voids on promises * PR feedback * PR feedback * Hide update project command from vscode * Swapping cross-extension commands for bound extension contract * Re-adding schema compare radio buttons for sqlproj * Adding refresh after project update * Populating list of project scripts just before comparison to avoid missing script errors of project was separately edited * Adding missing await for okay button enable check * Correcting schema compare source when populated from a project * Rename UpdateDataModel to be more clear * Fix incorrectly changed type * Added new runComparison schema compare command, hooked up to sqlproj extension * Added progress indicator for "apply now" option * moved string literal to constant * Added missing await * Setting missing "saveScmpButton" state to fix test * Revert "Setting missing "saveScmpButton" state to fix test" This reverts commit 55612c9def24ac9e3398f5bbd153d21d9d3ca37f. * Removing preemptive resetWindow() call * general cleanup * PR feedback * property renames * Reverting rename; requires Tools Service change first * Adding header to updateProject * Adding missing header * PR feedback * adding missing await * Handing race condition for UI enable * Fixing broken okay enable case * Fixing enum comparison wonk Co-authored-by: Noureldine Yehia <t-nyehia@microsoft.com>
This commit is contained in:
@@ -38,6 +38,10 @@
|
||||
"light": "./images/light_icon.svg",
|
||||
"dark": "./images/dark_icon.svg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "schemaCompare.runComparison",
|
||||
"title": "%schemaCompare.runComparison%"
|
||||
}
|
||||
],
|
||||
"languages": [
|
||||
@@ -83,6 +87,10 @@
|
||||
{
|
||||
"command": "schemaCompare.start",
|
||||
"when": "mssql:engineedition != 11"
|
||||
},
|
||||
{
|
||||
"command": "schemaCompare.runComparison",
|
||||
"when": "false"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"displayName": "SQL Server Schema Compare",
|
||||
"description": "SQL Server Schema Compare for Azure Data Studio supports comparing the schemas of databases and dacpacs.",
|
||||
"schemaCompare.start": "Schema Compare"
|
||||
}
|
||||
"schemaCompare.start": "Schema Compare",
|
||||
"schemaCompare.runComparison": "Run Schema Comparison"
|
||||
}
|
||||
|
||||
@@ -5,12 +5,13 @@
|
||||
|
||||
import * as azdata from 'azdata';
|
||||
import * as vscode from 'vscode';
|
||||
import * as loc from '../localizedConstants';
|
||||
import * as path from 'path';
|
||||
import * as sqldbproj from 'sqldbproj';
|
||||
import * as mssql from '../../../mssql';
|
||||
import * as loc from '../localizedConstants';
|
||||
import { SchemaCompareMainWindow } from '../schemaCompareMainWindow';
|
||||
import { TelemetryReporter, TelemetryViews } from '../telemetry';
|
||||
import { getEndpointName, getRootPath, exists, getAzdataApi, getSchemaCompareEndpointString } from '../utils';
|
||||
import * as mssql from '../../../mssql';
|
||||
|
||||
const titleFontSize: number = 13;
|
||||
|
||||
@@ -141,8 +142,8 @@ export class SchemaCompareDialog {
|
||||
this.schemaCompareMainWindow.sourceEndpointInfo = {
|
||||
endpointType: mssql.SchemaCompareEndpointType.Project,
|
||||
projectFilePath: this.sourceTextBox.value,
|
||||
targetScripts: await this.getTargetScripts(true),
|
||||
dataSchemaProvider: await this.getDsp(this.sourceTextBox.value),
|
||||
targetScripts: await this.getProjectScriptFiles(this.sourceTextBox.value),
|
||||
dataSchemaProvider: await this.getDatabaseSchemaProvider(this.sourceTextBox.value),
|
||||
folderStructure: '',
|
||||
serverDisplayName: '',
|
||||
serverName: '',
|
||||
@@ -190,8 +191,8 @@ export class SchemaCompareDialog {
|
||||
endpointType: mssql.SchemaCompareEndpointType.Project,
|
||||
projectFilePath: this.targetTextBox.value,
|
||||
folderStructure: this.targetStructureDropdown!.value as string,
|
||||
targetScripts: await this.getTargetScripts(false),
|
||||
dataSchemaProvider: await this.getDsp(this.targetTextBox.value),
|
||||
targetScripts: await this.getProjectScriptFiles(this.targetTextBox.value),
|
||||
dataSchemaProvider: await this.getDatabaseSchemaProvider(this.targetTextBox.value),
|
||||
serverDisplayName: '',
|
||||
serverName: '',
|
||||
databaseName: '',
|
||||
@@ -528,10 +529,9 @@ export class SchemaCompareDialog {
|
||||
|
||||
let radioButtons = [this.sourceDatabaseRadioButton, this.sourceDacpacRadioButton];
|
||||
|
||||
// TODO: re-add once database projects changes are checked in; chicken-and-egg problem (https://github.com/microsoft/azuredatastudio/pull/17738)
|
||||
// if (vscode.extensions.getExtension(loc.sqlDatabaseProjectExtensionId)) {
|
||||
// radioButtons.push(this.sourceProjectRadioButton);
|
||||
// }
|
||||
if (vscode.extensions.getExtension(loc.sqlDatabaseProjectExtensionId)) {
|
||||
radioButtons.push(this.sourceProjectRadioButton);
|
||||
}
|
||||
|
||||
let flexRadioButtonsModel = this.view.modelBuilder.flexContainer()
|
||||
.withLayout({ flexFlow: 'column' })
|
||||
@@ -617,10 +617,9 @@ export class SchemaCompareDialog {
|
||||
|
||||
let radioButtons = [targetDatabaseRadioButton, targetDacpacRadioButton];
|
||||
|
||||
// TODO: re-add once database projects changes are checked in; chicken-and-egg problem (https://github.com/microsoft/azuredatastudio/pull/17738)
|
||||
// if (vscode.extensions.getExtension(loc.sqlDatabaseProjectExtensionId)) {
|
||||
// radioButtons.push(targetProjectRadioButton);
|
||||
// }
|
||||
if (vscode.extensions.getExtension(loc.sqlDatabaseProjectExtensionId)) {
|
||||
radioButtons.push(targetProjectRadioButton);
|
||||
}
|
||||
|
||||
let flexRadioButtonsModel = this.view.modelBuilder.flexContainer()
|
||||
.withLayout({ flexFlow: 'column' })
|
||||
@@ -636,10 +635,10 @@ export class SchemaCompareDialog {
|
||||
|
||||
private async shouldEnableOkayButton(): Promise<boolean> {
|
||||
let sourcefilled = (this.sourceEndpointType === mssql.SchemaCompareEndpointType.Dacpac && await this.existsDacpac(this.sourceTextBox.value))
|
||||
|| (this.sourceEndpointType === mssql.SchemaCompareEndpointType.Project && this.existsProjectFile(this.sourceTextBox.value))
|
||||
|| (this.sourceEndpointType === mssql.SchemaCompareEndpointType.Project && await this.existsProjectFile(this.sourceTextBox.value))
|
||||
|| (this.sourceEndpointType === mssql.SchemaCompareEndpointType.Database && !isNullOrUndefined(this.sourceDatabaseDropdown.value) && this.sourceDatabaseDropdown.values.findIndex(x => this.matchesValue(x, this.sourceDbEditable)) !== -1);
|
||||
let targetfilled = (this.targetEndpointType === mssql.SchemaCompareEndpointType.Dacpac && await this.existsDacpac(this.targetTextBox.value))
|
||||
|| (this.targetEndpointType === mssql.SchemaCompareEndpointType.Project && this.existsProjectFile(this.targetTextBox.value))
|
||||
|| (this.targetEndpointType === mssql.SchemaCompareEndpointType.Project && await this.existsProjectFile(this.targetTextBox.value))
|
||||
|| (this.targetEndpointType === mssql.SchemaCompareEndpointType.Database && !isNullOrUndefined(this.targetDatabaseDropdown.value) && this.targetDatabaseDropdown.values.findIndex(x => this.matchesValue(x, this.targetDbEditable)) !== -1);
|
||||
|
||||
return sourcefilled && targetfilled;
|
||||
@@ -670,7 +669,7 @@ export class SchemaCompareDialog {
|
||||
// check Database Schema Providers are set and valid
|
||||
if (this.sourceEndpointType === mssql.SchemaCompareEndpointType.Project) {
|
||||
try {
|
||||
await this.getDsp(this.sourceTextBox.value);
|
||||
await this.getDatabaseSchemaProvider(this.sourceTextBox.value);
|
||||
} catch (err) {
|
||||
this.showErrorMessage(loc.dspErrorSource);
|
||||
}
|
||||
@@ -678,7 +677,7 @@ export class SchemaCompareDialog {
|
||||
|
||||
if (this.targetEndpointType === mssql.SchemaCompareEndpointType.Project) {
|
||||
try {
|
||||
await this.getDsp(this.targetTextBox.value);
|
||||
await this.getDatabaseSchemaProvider(this.targetTextBox.value);
|
||||
} catch (err) {
|
||||
this.showErrorMessage(loc.dspErrorTarget);
|
||||
}
|
||||
@@ -703,13 +702,20 @@ export class SchemaCompareDialog {
|
||||
return !isNullOrUndefined(filename) && await exists(filename) && (filename.toLocaleLowerCase().endsWith('.sqlproj'));
|
||||
}
|
||||
|
||||
private async getTargetScripts(source: boolean): Promise<string[]> {
|
||||
const projectFilePath = source ? this.sourceTextBox.value : this.targetTextBox.value;
|
||||
return await vscode.commands.executeCommand(loc.sqlDatabaseProjectsGetTargetScripts, projectFilePath);
|
||||
private async getProjectScriptFiles(projectFilePath: string): Promise<string[]> {
|
||||
const databaseProjectsExtension = vscode.extensions.getExtension(loc.sqlDatabaseProjectExtensionId);
|
||||
|
||||
if (databaseProjectsExtension) {
|
||||
return await (await databaseProjectsExtension.activate() as sqldbproj.IExtension).getProjectScriptFiles(projectFilePath);
|
||||
}
|
||||
}
|
||||
|
||||
private async getDsp(projectFilePath: string): Promise<string> {
|
||||
return await vscode.commands.executeCommand(loc.sqlDatabaseProjectsGetDsp, projectFilePath);
|
||||
private async getDatabaseSchemaProvider(projectFilePath: string): Promise<string> {
|
||||
const databaseProjectsExtension = vscode.extensions.getExtension(loc.sqlDatabaseProjectExtensionId);
|
||||
|
||||
if (databaseProjectsExtension) {
|
||||
return await (await databaseProjectsExtension.activate() as sqldbproj.IExtension).getProjectDatabaseSchemaProvider(projectFilePath);
|
||||
}
|
||||
}
|
||||
|
||||
protected createSourceServerDropdown(): azdata.FormComponent {
|
||||
|
||||
@@ -4,10 +4,12 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import * as mssql from '../../mssql/src/mssql';
|
||||
import { SchemaCompareMainWindow } from './schemaCompareMainWindow';
|
||||
|
||||
export async function activate(extensionContext: vscode.ExtensionContext): Promise<void> {
|
||||
vscode.commands.registerCommand('schemaCompare.start', async (sourceContext: any, targetContext: any = undefined, comparisonResult: any = undefined) => { await new SchemaCompareMainWindow(undefined, extensionContext, undefined).start(sourceContext, targetContext, comparisonResult); });
|
||||
vscode.commands.registerCommand('schemaCompare.runComparison', async (source: mssql.SchemaCompareEndpointInfo | undefined, target: mssql.SchemaCompareEndpointInfo | undefined, runComparison: boolean = false, comparisonResult: mssql.SchemaCompareResult | undefined) => { await new SchemaCompareMainWindow(undefined, extensionContext, undefined).launch(source, target, runComparison, comparisonResult); });
|
||||
}
|
||||
|
||||
export function deactivate(): void {
|
||||
|
||||
@@ -339,7 +339,4 @@ export const applySuccess: string = localize('schemaCompare.applySuccess', "Proj
|
||||
export const sqlDatabaseProjectExtensionId: string = 'microsoft.sql-database-projects';
|
||||
|
||||
// Commands
|
||||
export const sqlDatabaseProjectsGetTargetScripts: string = 'sqlDatabaseProjects.schemaCompareGetTargetScripts';
|
||||
export const sqlDatabaseProjectsGetDsp: string = 'sqlDatabaseProjects.schemaCompareGetDsp';
|
||||
export const sqlDatabaseProjectsPublishChanges: string = 'sqlDatabaseProjects.schemaComparePublishProjectChanges';
|
||||
export const sqlDatabaseProjectsShowProjectsView: string = 'sqlDatabaseProjects.schemaCompareShowProjectsView';
|
||||
|
||||
@@ -7,11 +7,12 @@ import * as azdata from 'azdata';
|
||||
import * as vscode from 'vscode';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import * as sqldbproj from 'sqldbproj';
|
||||
import * as mssql from '../../mssql';
|
||||
import * as loc from './localizedConstants';
|
||||
import { SchemaCompareOptionsDialog } from './dialogs/schemaCompareOptionsDialog';
|
||||
import { TelemetryReporter, TelemetryViews } from './telemetry';
|
||||
import { getTelemetryErrorType, getEndpointName, verifyConnectionAndGetOwnerUri, getRootPath, getSchemaCompareEndpointString } from './utils';
|
||||
import { getTelemetryErrorType, getEndpointName, verifyConnectionAndGetOwnerUri, getRootPath, getSchemaCompareEndpointString, getDataWorkspaceExtensionApi } from './utils';
|
||||
import { SchemaCompareDialog } from './dialogs/schemaCompareDialog';
|
||||
import { isNullOrUndefined } from 'util';
|
||||
|
||||
@@ -87,6 +88,9 @@ export class SchemaCompareMainWindow {
|
||||
// 3. dacpac
|
||||
// 4. project
|
||||
public async start(sourceContext: any, targetContext: mssql.SchemaCompareEndpointInfo = undefined, comparisonResult: mssql.SchemaCompareResult = undefined): Promise<void> {
|
||||
let source: mssql.SchemaCompareEndpointInfo;
|
||||
let target: mssql.SchemaCompareEndpointInfo;
|
||||
|
||||
const targetIsSetAsProject: boolean = targetContext && targetContext.endpointType === mssql.SchemaCompareEndpointType.Project;
|
||||
|
||||
// if schema compare was launched from a db or a connection profile, set that as the source
|
||||
@@ -94,7 +98,7 @@ export class SchemaCompareMainWindow {
|
||||
|
||||
if (targetIsSetAsProject) {
|
||||
profile = sourceContext;
|
||||
this.targetEndpointInfo = targetContext;
|
||||
target = targetContext;
|
||||
} else {
|
||||
profile = sourceContext ? <azdata.IConnectionProfile>sourceContext.connectionProfile : undefined;
|
||||
}
|
||||
@@ -115,7 +119,7 @@ export class SchemaCompareMainWindow {
|
||||
usr = loc.defaultText;
|
||||
}
|
||||
|
||||
this.sourceEndpointInfo = {
|
||||
source = {
|
||||
endpointType: mssql.SchemaCompareEndpointType.Database,
|
||||
serverDisplayName: `${profile.serverName} (${usr})`,
|
||||
serverName: profile.serverName,
|
||||
@@ -130,7 +134,7 @@ export class SchemaCompareMainWindow {
|
||||
folderStructure: ''
|
||||
};
|
||||
} else if (sourceDacpac) {
|
||||
this.sourceEndpointInfo = {
|
||||
source = {
|
||||
endpointType: mssql.SchemaCompareEndpointType.Dacpac,
|
||||
serverDisplayName: '',
|
||||
serverName: '',
|
||||
@@ -144,7 +148,7 @@ export class SchemaCompareMainWindow {
|
||||
folderStructure: ''
|
||||
};
|
||||
} else if (sourceProject) {
|
||||
this.sourceEndpointInfo = {
|
||||
source = {
|
||||
endpointType: mssql.SchemaCompareEndpointType.Project,
|
||||
packageFilePath: '',
|
||||
serverDisplayName: '',
|
||||
@@ -159,14 +163,38 @@ export class SchemaCompareMainWindow {
|
||||
};
|
||||
}
|
||||
|
||||
await this.launch(source, target, false, comparisonResult);
|
||||
}
|
||||
|
||||
/**
|
||||
* Primary functional entrypoint for opening the schema comparison window, and optionally running it.
|
||||
* @param source
|
||||
* @param target
|
||||
* @param runComparison whether to immediately run the schema comparison. Requires both source and target to be specified. Cannot be true when comparisonResult is set.
|
||||
* @param comparisonResult a pre-computed schema comparison result to display. Cannot be set when runComparison is true.
|
||||
*/
|
||||
public async launch(source: mssql.SchemaCompareEndpointInfo | undefined, target: mssql.SchemaCompareEndpointInfo | undefined, runComparison: boolean = false, comparisonResult: mssql.SchemaCompareResult | undefined): Promise<void> {
|
||||
if (runComparison && comparisonResult) {
|
||||
throw new Error('Cannot both pass a comparison result and request a new comparison be run.');
|
||||
}
|
||||
|
||||
this.sourceEndpointInfo = source;
|
||||
this.targetEndpointInfo = target;
|
||||
|
||||
await this.GetDefaultDeploymentOptions();
|
||||
await Promise.all([
|
||||
this.registerContent(),
|
||||
this.editor.openEditor()
|
||||
]);
|
||||
|
||||
if (targetIsSetAsProject) {
|
||||
if (comparisonResult) {
|
||||
await this.execute(comparisonResult);
|
||||
} else if (runComparison) {
|
||||
if (!source || !target) {
|
||||
throw new Error('source and target must both be set when runComparison is true.');
|
||||
}
|
||||
|
||||
await this.startCompare();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -321,6 +349,18 @@ export class SchemaCompareMainWindow {
|
||||
this.deploymentOptions = deploymentOptions;
|
||||
}
|
||||
|
||||
private async populateProjectScripts(endpointInfo: mssql.SchemaCompareEndpointInfo): Promise<void> {
|
||||
if (endpointInfo.endpointType !== mssql.SchemaCompareEndpointType.Project) {
|
||||
return;
|
||||
}
|
||||
|
||||
const databaseProjectsExtension = vscode.extensions.getExtension(loc.sqlDatabaseProjectExtensionId);
|
||||
|
||||
if (databaseProjectsExtension) {
|
||||
endpointInfo.targetScripts = await (await databaseProjectsExtension.activate() as sqldbproj.IExtension).getProjectScriptFiles(endpointInfo.projectFilePath);
|
||||
}
|
||||
}
|
||||
|
||||
public async execute(comparisonResult: mssql.SchemaCompareCompletionResult = undefined) {
|
||||
const service = await this.getService();
|
||||
|
||||
@@ -336,6 +376,8 @@ export class SchemaCompareMainWindow {
|
||||
this.operationId = generateGuid();
|
||||
}
|
||||
|
||||
await Promise.all([this.populateProjectScripts(this.sourceEndpointInfo), this.populateProjectScripts(this.targetEndpointInfo)]);
|
||||
|
||||
this.comparisonResult = await service.schemaCompare(this.operationId, this.sourceEndpointInfo, this.targetEndpointInfo, azdata.TaskExecutionMode.execute, this.deploymentOptions);
|
||||
|
||||
if (!this.comparisonResult || !this.comparisonResult.success) {
|
||||
@@ -831,7 +873,8 @@ export class SchemaCompareMainWindow {
|
||||
TelemetryReporter.createActionEvent(TelemetryViews.SchemaCompareMainWindow, 'SchemaCompareApplyStarted')
|
||||
.withAdditionalProperties({
|
||||
'startTime': Date.now().toString(),
|
||||
'operationId': this.comparisonResult.operationId
|
||||
'operationId': this.comparisonResult.operationId,
|
||||
'targetType': getSchemaCompareEndpointString(this.targetEndpointInfo.endpointType)
|
||||
}).send();
|
||||
|
||||
// disable apply and generate script buttons because the results are no longer valid after applying the changes
|
||||
@@ -844,7 +887,12 @@ export class SchemaCompareMainWindow {
|
||||
case mssql.SchemaCompareEndpointType.Database:
|
||||
result = await service.schemaComparePublishDatabaseChanges(this.comparisonResult.operationId, this.targetEndpointInfo.serverName, this.targetEndpointInfo.databaseName, azdata.TaskExecutionMode.execute);
|
||||
break;
|
||||
case mssql.SchemaCompareEndpointType.Project: // Project apply needs sql-database-projects updates in (circular dependency; coming next) // TODO: re-add this and show project logic below
|
||||
case mssql.SchemaCompareEndpointType.Project:
|
||||
result = await vscode.commands.executeCommand(loc.sqlDatabaseProjectsPublishChanges, this.comparisonResult.operationId, this.targetEndpointInfo.projectFilePath, this.targetEndpointInfo.folderStructure);
|
||||
if (!result.success) {
|
||||
void vscode.window.showErrorMessage(loc.applyError);
|
||||
}
|
||||
break;
|
||||
case mssql.SchemaCompareEndpointType.Dacpac: // Dacpac is an invalid publish target
|
||||
default:
|
||||
throw new Error(`Unsupported SchemaCompareEndpointType: ${getSchemaCompareEndpointString(this.targetEndpointInfo.endpointType)}`);
|
||||
@@ -854,7 +902,8 @@ export class SchemaCompareMainWindow {
|
||||
|
||||
TelemetryReporter.createErrorEvent(TelemetryViews.SchemaCompareMainWindow, 'SchemaCompareApplyFailed', undefined, getTelemetryErrorType(result?.errorMessage))
|
||||
.withAdditionalProperties({
|
||||
'operationId': this.comparisonResult.operationId
|
||||
'operationId': this.comparisonResult.operationId,
|
||||
'targetType': getSchemaCompareEndpointString(this.targetEndpointInfo.endpointType)
|
||||
}).send();
|
||||
vscode.window.showErrorMessage(loc.applyErrorMessage(result?.errorMessage));
|
||||
|
||||
@@ -868,8 +917,16 @@ export class SchemaCompareMainWindow {
|
||||
TelemetryReporter.createActionEvent(TelemetryViews.SchemaCompareMainWindow, 'SchemaCompareApplyEnded')
|
||||
.withAdditionalProperties({
|
||||
'endTime': Date.now().toString(),
|
||||
'operationId': this.comparisonResult.operationId
|
||||
'operationId': this.comparisonResult.operationId,
|
||||
'targetType': getSchemaCompareEndpointString(this.targetEndpointInfo.endpointType)
|
||||
}).send();
|
||||
|
||||
if (this.targetEndpointInfo.endpointType === mssql.SchemaCompareEndpointType.Project) {
|
||||
const workspaceApi = getDataWorkspaceExtensionApi();
|
||||
workspaceApi.showProjectsView();
|
||||
|
||||
void vscode.window.showInformationMessage(loc.applySuccess);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -6,4 +6,6 @@
|
||||
/// <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'/>
|
||||
/// <reference path='../../../data-workspace/src/dataworkspace.d.ts'/>
|
||||
/// <reference path='../../../sql-database-projects/src/sqldbproj.d.ts'/>
|
||||
/// <reference types='@types/node'/>
|
||||
|
||||
@@ -9,6 +9,7 @@ import * as vscode from 'vscode';
|
||||
import * as mssql from '../../mssql';
|
||||
import * as os from 'os';
|
||||
import * as loc from './localizedConstants';
|
||||
import * as dataworkspace from 'dataworkspace';
|
||||
import { promises as fs } from 'fs';
|
||||
|
||||
export interface IPackageInfo {
|
||||
@@ -182,3 +183,8 @@ try {
|
||||
export function getAzdataApi(): typeof azdataType | undefined {
|
||||
return azdataApi;
|
||||
}
|
||||
|
||||
export function getDataWorkspaceExtensionApi(): dataworkspace.IExtension {
|
||||
const extension = vscode.extensions.getExtension(dataworkspace.extension.name)!;
|
||||
return extension.exports;
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
"onCommand:sqlDatabaseProjects.new",
|
||||
"onCommand:sqlDatabaseProjects.open",
|
||||
"onCommand:sqlDatabaseProjects.createProjectFromDatabase",
|
||||
"onCommand:sqlDatabaseProjects.updateProjectFromDatabase",
|
||||
"onCommand:sqlDatabaseProjects.addSqlBinding",
|
||||
"onCommand:sqlDatabaseProjects.generateProjectFromOpenApiSpec",
|
||||
"onCommand:sqlDatabaseProjects.addSqlBinding",
|
||||
"workspaceContains:**/*.sqlproj",
|
||||
@@ -145,6 +147,12 @@
|
||||
"category": "%sqlDatabaseProjects.displayName%",
|
||||
"icon": "images/databaseProjectToolbar.svg"
|
||||
},
|
||||
{
|
||||
"command": "sqlDatabaseProjects.updateProjectFromDatabase",
|
||||
"title": "%sqlDatabaseProjects.updateProjectFromDatabase%",
|
||||
"category": "%sqlDatabaseProjects.displayName%",
|
||||
"icon": "images/databaseProjectToolbar.svg"
|
||||
},
|
||||
{
|
||||
"command": "sqlDatabaseProjects.addDatabaseReference",
|
||||
"title": "%sqlDatabaseProjects.addDatabaseReference%",
|
||||
@@ -188,6 +196,11 @@
|
||||
"when": "view == dataworkspace.views.main",
|
||||
"group": "1_currentWorkspace@1"
|
||||
},
|
||||
{
|
||||
"command": "sqlDatabaseProjects.updateProjectFromDatabase",
|
||||
"when": "view == dataworkspace.views.main",
|
||||
"group": "1_currentWorkspace@2"
|
||||
},
|
||||
{
|
||||
"command": "sqlDatabaseProjects.generateProjectFromOpenApiSpec",
|
||||
"when": "view == dataworkspace.views.main",
|
||||
@@ -254,6 +267,10 @@
|
||||
{
|
||||
"command": "sqlDatabaseProjects.createProjectFromDatabase"
|
||||
},
|
||||
{
|
||||
"command": "sqlDatabaseProjects.updateProjectFromDatabase",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "sqlDatabaseProjects.addDatabaseReference",
|
||||
"when": "false"
|
||||
@@ -299,6 +316,11 @@
|
||||
"when": "view == dataworkspace.views.main && viewItem == databaseProject.itemType.project && azdataAvailable",
|
||||
"group": "1_dbProjectsFirst@3"
|
||||
},
|
||||
{
|
||||
"command": "sqlDatabaseProjects.updateProjectFromDatabase",
|
||||
"when": "view == dataworkspace.views.main && viewItem == databaseProject.itemType.project && azdataAvailable",
|
||||
"group": "1_dbProjectsFirst@4"
|
||||
},
|
||||
{
|
||||
"command": "sqlDatabaseProjects.newItem",
|
||||
"when": "view == dataworkspace.views.main && viewItem == databaseProject.itemType.project || viewItem == databaseProject.itemType.folder",
|
||||
@@ -389,20 +411,34 @@
|
||||
{
|
||||
"command": "sqlDatabaseProjects.createProjectFromDatabase",
|
||||
"when": "nodeType =~ /^(Database|Server)$/ && connectionProvider == MSSQL && mssql:engineedition != 11",
|
||||
"group": "export"
|
||||
"group": "export@1"
|
||||
},
|
||||
{
|
||||
"command": "sqlDatabaseProjects.updateProjectFromDatabase",
|
||||
"when": "nodeType =~ /^(Database|Server)$/ && connectionProvider == MSSQL && mssql:engineedition != 11",
|
||||
"group": "export@2"
|
||||
}
|
||||
],
|
||||
"dataExplorer/context": [
|
||||
{
|
||||
"command": "sqlDatabaseProjects.createProjectFromDatabase",
|
||||
"when": "nodeType =~ /^(Database|Server)$/ && connectionProvider == MSSQL && mssql:engineedition != 11",
|
||||
"group": "export"
|
||||
"group": "export@1"
|
||||
},
|
||||
{
|
||||
"command": "sqlDatabaseProjects.updateProjectFromDatabase",
|
||||
"when": "nodeType =~ /^(Database|Server)$/ && connectionProvider == MSSQL && mssql:engineedition != 11",
|
||||
"group": "export@2"
|
||||
}
|
||||
],
|
||||
"dashboard/toolbar": [
|
||||
{
|
||||
"command": "sqlDatabaseProjects.createProjectFromDatabase",
|
||||
"when": "connectionProvider == 'MSSQL' && mssql:engineedition != 11"
|
||||
},
|
||||
{
|
||||
"command": "sqlDatabaseProjects.updateProjectFromDatabase",
|
||||
"when": "connectionProvider == 'MSSQL' && mssql:engineedition != 11"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"sqlDatabaseProjects.build": "Build",
|
||||
"sqlDatabaseProjects.publish": "Publish",
|
||||
"sqlDatabaseProjects.createProjectFromDatabase": "Create Project From Database",
|
||||
"sqlDatabaseProjects.updateProjectFromDatabase": "Update Project From Database",
|
||||
"sqlDatabaseProjects.properties": "Properties",
|
||||
"sqlDatabaseProjects.schemaCompare": "Schema Compare",
|
||||
"sqlDatabaseProjects.delete": "Delete",
|
||||
|
||||
@@ -62,7 +62,9 @@ export const at = localize('at', "at");
|
||||
// commands
|
||||
export const revealFileInOsCommand = 'revealFileInOS';
|
||||
export const schemaCompareStartCommand = 'schemaCompare.start';
|
||||
export const schemaCompareRunComparisonCommand = 'schemaCompare.runComparison';
|
||||
export const vscodeOpenCommand = 'vscode.open';
|
||||
export const refreshDataWorkspaceCommand = 'dataworkspace.refresh';
|
||||
|
||||
// UI Strings
|
||||
|
||||
@@ -241,6 +243,7 @@ export const targetProject = localize('targetProject', "Target project");
|
||||
export const createProjectSettings = localize('createProjectSettings', "Settings");
|
||||
export const projectNameLabel = localize('projectNameLabel', "Name");
|
||||
export const projectNamePlaceholderText = localize('projectNamePlaceholderText', "Enter project name");
|
||||
export const projectLocationLabel = localize('projectLocationLabel', "Location");
|
||||
export const projectLocationPlaceholderText = localize('projectLocationPlaceholderText', "Select location to create project");
|
||||
export const browseButtonText = localize('browseButtonText', "Browse folder");
|
||||
export const selectFolderStructure = localize('selectFolderStructure', "Select folder structure");
|
||||
@@ -251,9 +254,28 @@ export const selectProjectLocation = localize('selectProjectLocation', "Select p
|
||||
export const ProjectParentDirectoryNotExistError = (location: string): string => { return localize('dataworkspace.projectParentDirectoryNotExistError', "The selected project location '{0}' does not exist or is not a directory.", location); };
|
||||
export const ProjectDirectoryAlreadyExistError = (projectName: string, location: string): string => { return localize('dataworkspace.projectDirectoryAlreadyExistError', "There is already a directory named '{0}' in the selected location: '{1}'.", projectName, location); };
|
||||
|
||||
// Update Project From Database dialog strings
|
||||
|
||||
export const updateProjectFromDatabaseDialogName = localize('updateProjectFromDatabaseDialogName', "Update project from database");
|
||||
export const updateText = localize('updateText', "Update");
|
||||
export const noSqlProjFile = localize('noSqlProjFile', "The selected project file does not exist");
|
||||
export const noSchemaCompareExtension = localize('noSchemaCompareExtension', "The Schema Compare extension must be installed to a update a project from a database.");
|
||||
export const projectToUpdatePlaceholderText = localize('projectToUpdatePlaceholderText', "Select project file");
|
||||
export const updateAction = localize('updateAction', "Update action");
|
||||
export const compareActionRadioButtonLabel = localize('compareActionRadiButtonLabel', "View changes in Schema Compare");
|
||||
export const updateActionRadioButtonLabel = localize('updateActionRadiButtonLabel', "Apply all changes");
|
||||
export const actionLabel = localize('actionLabel', "Action");
|
||||
|
||||
// Update project from database
|
||||
|
||||
export const applySuccess = localize('applySuccess', "Project was successfully updated.");
|
||||
export const equalComparison = localize('equalComparison', "The project is already up to date with the database.");
|
||||
export function applyError(errorMessage: string): string { return localize('applyError', "There was an error updating the project: {0}", errorMessage); }
|
||||
export function updatingProjectFromDatabase(projectName: string, databaseName: string): string { return localize('updatingProjectFromDatabase', "Updating {0} from {1}...", projectName, databaseName); }
|
||||
|
||||
// Error messages
|
||||
|
||||
export function compareErrorMessage(errorMessage: string): string { return localize('schemaCompare.compareErrorMessage', "Schema Compare failed: {0}", errorMessage ? errorMessage : 'Unknown'); }
|
||||
export const multipleSqlProjFiles = localize('multipleSqlProjFilesSelected', "Multiple .sqlproj files selected; please select only one.");
|
||||
export const noSqlProjFiles = localize('noSqlProjFilesSelected', "No .sqlproj file selected; please select one.");
|
||||
export const noDataSourcesFile = localize('noDataSourcesFile', "No {0} found", dataSourcesFileName);
|
||||
|
||||
@@ -20,6 +20,9 @@ export namespace cssStyles {
|
||||
export const createProjectFromDatabaseLabelWidth = '110px';
|
||||
export const createProjectFromDatabaseTextboxWidth = '310px';
|
||||
|
||||
export const updateProjectFromDatabaseLabelWidth = '110px';
|
||||
export const updateProjectFromDatabaseTextboxWidth = '310px';
|
||||
|
||||
// font-styles
|
||||
export namespace fontStyle {
|
||||
export const normal = 'normal';
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import type * as azdataType from 'azdata';
|
||||
import * as vscode from 'vscode';
|
||||
import * as vscodeMssql from 'vscode-mssql';
|
||||
import * as mssql from '../../../mssql';
|
||||
import * as templates from '../templates/templates';
|
||||
import * as path from 'path';
|
||||
|
||||
@@ -64,6 +65,8 @@ export default class MainController implements vscode.Disposable {
|
||||
vscode.commands.registerCommand('sqlDatabaseProjects.build', async (node: WorkspaceTreeItem) => { return this.projectsController.buildProject(node); });
|
||||
vscode.commands.registerCommand('sqlDatabaseProjects.publish', async (node: WorkspaceTreeItem) => { return this.projectsController.publishProject(node); });
|
||||
vscode.commands.registerCommand('sqlDatabaseProjects.schemaCompare', async (node: WorkspaceTreeItem) => { return this.projectsController.schemaCompare(node); });
|
||||
vscode.commands.registerCommand('sqlDatabaseProjects.schemaComparePublishProjectChanges', async (operationId: string, projectFilePath: string, folderStructure: string): Promise<mssql.SchemaComparePublishProjectResult> => { return await this.projectsController.schemaComparePublishProjectChanges(operationId, projectFilePath, folderStructure); });
|
||||
vscode.commands.registerCommand('sqlDatabaseProjects.updateProjectFromDatabase', async (node: azdataType.IConnectionProfile | vscodeMssql.ITreeNodeInfo | WorkspaceTreeItem) => { await this.projectsController.updateProjectFromDatabase(node); });
|
||||
vscode.commands.registerCommand('sqlDatabaseProjects.createProjectFromDatabase', async (context: azdataType.IConnectionProfile | vscodeMssql.ITreeNodeInfo | undefined) => { return this.projectsController.createProjectFromDatabase(context); });
|
||||
vscode.commands.registerCommand('sqlDatabaseProjects.generateProjectFromOpenApiSpec', async (options?: GenerateProjectFromOpenApiSpecOptions) => { return this.projectsController.generateProjectFromOpenApiSpec(options); });
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ import { AddDatabaseReferenceDialog } from '../dialogs/addDatabaseReferenceDialo
|
||||
import { ISystemDatabaseReferenceSettings, IDacpacReferenceSettings, IProjectReferenceSettings } from '../models/IDatabaseReferenceSettings';
|
||||
import { DatabaseReferenceTreeItem } from '../models/tree/databaseReferencesTreeItem';
|
||||
import { CreateProjectFromDatabaseDialog } from '../dialogs/createProjectFromDatabaseDialog';
|
||||
import { UpdateProjectFromDatabaseDialog } from '../dialogs/updateProjectFromDatabaseDialog';
|
||||
import { TelemetryActions, TelemetryReporter, TelemetryViews } from '../common/telemetry';
|
||||
import { IconPathHelper } from '../common/iconHelper';
|
||||
import { DashboardData, PublishData, Status } from '../models/dashboardData/dashboardData';
|
||||
@@ -43,7 +44,8 @@ import { AutorestHelper } from '../tools/autorestHelper';
|
||||
import { createNewProjectFromDatabaseWithQuickpick } from '../dialogs/createProjectFromDatabaseQuickpick';
|
||||
import { addDatabaseReferenceQuickpick } from '../dialogs/addDatabaseReferenceQuickpick';
|
||||
import { IDeployProfile } from '../models/deploy/deployProfile';
|
||||
import { FileProjectEntry, IDatabaseReferenceProjectEntry, SqlProjectReferenceProjectEntry } from '../models/projectEntry';
|
||||
import { EntryType, FileProjectEntry, IDatabaseReferenceProjectEntry, SqlProjectReferenceProjectEntry } from '../models/projectEntry';
|
||||
import { UpdateProjectAction, UpdateProjectDataModel } from '../models/api/updateProject';
|
||||
|
||||
const maxTableLength = 10;
|
||||
|
||||
@@ -453,23 +455,28 @@ export class ProjectsController {
|
||||
return result;
|
||||
}
|
||||
|
||||
public async schemaCompare(treeNode: dataworkspace.WorkspaceTreeItem): Promise<void> {
|
||||
public async schemaCompare(source: dataworkspace.WorkspaceTreeItem | azdataType.IConnectionProfile, targetParam: any = undefined): Promise<void> {
|
||||
try {
|
||||
// check if schema compare extension is installed
|
||||
if (vscode.extensions.getExtension(constants.schemaCompareExtensionId)) {
|
||||
// build project
|
||||
const dacpacPath = await this.buildProject(treeNode);
|
||||
let sourceParam;
|
||||
|
||||
// check that dacpac exists
|
||||
if (await utils.exists(dacpacPath)) {
|
||||
TelemetryReporter.sendActionEvent(TelemetryViews.ProjectController, TelemetryActions.projectSchemaCompareCommandInvoked);
|
||||
await vscode.commands.executeCommand(constants.schemaCompareStartCommand, dacpacPath);
|
||||
if (source as dataworkspace.WorkspaceTreeItem) {
|
||||
sourceParam = this.getProjectFromContext(source as dataworkspace.WorkspaceTreeItem).projectFilePath;
|
||||
} else {
|
||||
sourceParam = source as azdataType.IConnectionProfile;
|
||||
}
|
||||
|
||||
try {
|
||||
TelemetryReporter.sendActionEvent(TelemetryViews.ProjectController, TelemetryActions.projectSchemaCompareCommandInvoked);
|
||||
await vscode.commands.executeCommand(constants.schemaCompareStartCommand, sourceParam, targetParam, undefined);
|
||||
} catch (e) {
|
||||
throw new Error(constants.buildFailedCannotStartSchemaCompare);
|
||||
}
|
||||
} else {
|
||||
throw new Error(constants.schemaCompareNotInstalled);
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
const props: Record<string, string> = {};
|
||||
const message = utils.getErrorMessage(err);
|
||||
@@ -486,6 +493,67 @@ export class ProjectsController {
|
||||
}
|
||||
}
|
||||
|
||||
public async getProjectScriptFiles(projectFilePath: string): Promise<string[]> {
|
||||
const project = await Project.openProject(projectFilePath);
|
||||
|
||||
return project.files
|
||||
.filter(f => f.fsUri.fsPath.endsWith(constants.sqlFileExtension))
|
||||
.map(f => f.fsUri.fsPath);
|
||||
}
|
||||
|
||||
public async getProjectDatabaseSchemaProvider(projectFilePath: string): Promise<string> {
|
||||
const project = await Project.openProject(projectFilePath);
|
||||
return project.getProjectTargetVersion();
|
||||
}
|
||||
|
||||
public async schemaComparePublishProjectChanges(operationId: string, projectFilePath: string, folderStructure: string): Promise<mssql.SchemaComparePublishProjectResult> {
|
||||
const ext = vscode.extensions.getExtension(mssql.extension.name)!;
|
||||
const service = (await ext.activate() as mssql.IExtension).schemaCompare;
|
||||
|
||||
const projectPath = path.dirname(projectFilePath);
|
||||
|
||||
let fs: mssql.ExtractTarget;
|
||||
|
||||
switch (folderStructure) {
|
||||
case constants.file:
|
||||
fs = mssql.ExtractTarget.file;
|
||||
break;
|
||||
case constants.flat:
|
||||
fs = mssql.ExtractTarget.flat;
|
||||
break;
|
||||
case constants.objectType:
|
||||
fs = mssql.ExtractTarget.objectType;
|
||||
break;
|
||||
case constants.schema:
|
||||
fs = mssql.ExtractTarget.schema;
|
||||
break;
|
||||
case constants.schemaObjectType:
|
||||
default:
|
||||
fs = mssql.ExtractTarget.schemaObjectType;
|
||||
break;
|
||||
}
|
||||
|
||||
const result: mssql.SchemaComparePublishProjectResult = await service.schemaComparePublishProjectChanges(operationId, projectPath, fs, utils.getAzdataApi()!.TaskExecutionMode.execute);
|
||||
|
||||
const project = await Project.openProject(projectFilePath);
|
||||
|
||||
let toAdd: vscode.Uri[] = [];
|
||||
result.addedFiles.forEach((f: any) => toAdd.push(vscode.Uri.file(f)));
|
||||
await project.addToProject(toAdd);
|
||||
|
||||
let toRemove: vscode.Uri[] = [];
|
||||
result.deletedFiles.forEach((f: any) => toRemove.push(vscode.Uri.file(f)));
|
||||
|
||||
let toRemoveEntries: FileProjectEntry[] = [];
|
||||
toRemove.forEach(f => toRemoveEntries.push(new FileProjectEntry(f, f.path.replace(projectPath + '\\', ''), EntryType.File)));
|
||||
|
||||
toRemoveEntries.forEach(async f => await project.exclude(f));
|
||||
|
||||
await this.buildProject(project);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public async addFolderPrompt(treeNode: dataworkspace.WorkspaceTreeItem): Promise<void> {
|
||||
const project = this.getProjectFromContext(treeNode);
|
||||
const relativePathToParent = this.getRelativePath(treeNode.element);
|
||||
@@ -778,9 +846,6 @@ export class ProjectsController {
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1246,6 +1311,118 @@ export class ProjectsController {
|
||||
// TODO: Check for success; throw error
|
||||
}
|
||||
|
||||
/**
|
||||
* Display dialog for user to configure existing SQL Project with the changes/differences from a database
|
||||
*/
|
||||
public async updateProjectFromDatabase(context: azdataType.IConnectionProfile | mssqlVscode.ITreeNodeInfo | dataworkspace.WorkspaceTreeItem): Promise<UpdateProjectFromDatabaseDialog> {
|
||||
let connection: azdataType.IConnectionProfile | mssqlVscode.IConnectionInfo | undefined;
|
||||
let project: Project | undefined;
|
||||
|
||||
try {
|
||||
if ('connectionProfile' in context) {
|
||||
connection = this.getConnectionProfileFromContext(context as azdataType.IConnectionProfile | mssqlVscode.ITreeNodeInfo);
|
||||
}
|
||||
} catch { }
|
||||
|
||||
try {
|
||||
if ('treeDataProvider' in context) {
|
||||
project = this.getProjectFromContext(context as dataworkspace.WorkspaceTreeItem);
|
||||
}
|
||||
} catch { }
|
||||
|
||||
const updateProjectFromDatabaseDialog = this.getUpdateProjectFromDatabaseDialog(connection, project);
|
||||
|
||||
updateProjectFromDatabaseDialog.updateProjectFromDatabaseCallback = async (model) => await this.updateProjectFromDatabaseCallback(model);
|
||||
|
||||
await updateProjectFromDatabaseDialog.openDialog();
|
||||
|
||||
return updateProjectFromDatabaseDialog;
|
||||
}
|
||||
|
||||
public getUpdateProjectFromDatabaseDialog(connection: azdataType.IConnectionProfile | mssqlVscode.IConnectionInfo | undefined, project: Project | undefined): UpdateProjectFromDatabaseDialog {
|
||||
return new UpdateProjectFromDatabaseDialog(connection, project);
|
||||
}
|
||||
|
||||
public async updateProjectFromDatabaseCallback(model: UpdateProjectDataModel) {
|
||||
try {
|
||||
await this.updateProjectFromDatabaseApiCall(model);
|
||||
} catch (err) {
|
||||
void vscode.window.showErrorMessage(utils.getErrorMessage(err));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses the DacFx service to update an existing SQL Project with the changes/differences from a database
|
||||
*/
|
||||
public async updateProjectFromDatabaseApiCall(model: UpdateProjectDataModel): Promise<void> {
|
||||
if (model.action === UpdateProjectAction.Compare) {
|
||||
await vscode.commands.executeCommand(constants.schemaCompareRunComparisonCommand, model.sourceEndpointInfo, model.targetEndpointInfo, true, undefined);
|
||||
} else if (model.action === UpdateProjectAction.Update) {
|
||||
await vscode.window.withProgress(
|
||||
{
|
||||
location: vscode.ProgressLocation.Notification,
|
||||
title: constants.updatingProjectFromDatabase(path.basename(model.targetEndpointInfo.projectFilePath), model.sourceEndpointInfo.databaseName),
|
||||
cancellable: false
|
||||
}, async (_progress, _token) => {
|
||||
return this.schemaCompareAndUpdateProject(model.sourceEndpointInfo, model.targetEndpointInfo);
|
||||
});
|
||||
|
||||
void vscode.commands.executeCommand(constants.refreshDataWorkspaceCommand);
|
||||
utils.getDataWorkspaceExtensionApi().showProjectsView();
|
||||
} else {
|
||||
throw new Error(`Unknown UpdateProjectAction: ${model.action}`);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
private async schemaCompareAndUpdateProject(source: mssql.SchemaCompareEndpointInfo, target: mssql.SchemaCompareEndpointInfo): Promise<void> {
|
||||
// Run schema comparison
|
||||
const ext = vscode.extensions.getExtension(mssql.extension.name)!;
|
||||
const service = (await ext.activate() as mssql.IExtension).schemaCompare;
|
||||
const deploymentOptions = await service.schemaCompareGetDefaultOptions();
|
||||
const operationId = UUID.generateUuid();
|
||||
|
||||
target.targetScripts = await this.getProjectScriptFiles(target.projectFilePath);
|
||||
target.dataSchemaProvider = await this.getProjectDatabaseSchemaProvider(target.projectFilePath);
|
||||
|
||||
TelemetryReporter.sendActionEvent(TelemetryViews.ProjectController, 'SchemaComparisonStarted');
|
||||
|
||||
// Perform schema comparison. Results are cached in SqlToolsService under the operationId
|
||||
const comparisonResult: mssql.SchemaCompareResult = await service.schemaCompare(
|
||||
operationId, source, target, utils.getAzdataApi()!.TaskExecutionMode.execute, deploymentOptions.defaultDeploymentOptions
|
||||
);
|
||||
|
||||
if (!comparisonResult || !comparisonResult.success) {
|
||||
TelemetryReporter.createErrorEvent(TelemetryViews.ProjectController, 'SchemaComparisonFailed')
|
||||
.withAdditionalProperties({
|
||||
operationId: comparisonResult.operationId
|
||||
}).send();
|
||||
await vscode.window.showErrorMessage(constants.compareErrorMessage(comparisonResult?.errorMessage));
|
||||
return;
|
||||
}
|
||||
|
||||
TelemetryReporter.createActionEvent(TelemetryViews.ProjectController, 'SchemaComparisonFinished')
|
||||
.withAdditionalProperties({
|
||||
'endTime': Date.now().toString(),
|
||||
'operationId': comparisonResult.operationId
|
||||
}).send();
|
||||
|
||||
if (comparisonResult.areEqual) {
|
||||
void vscode.window.showInformationMessage(constants.equalComparison);
|
||||
return;
|
||||
}
|
||||
|
||||
// Publish the changes (retrieved from the cache by operationId)
|
||||
const publishResult = await this.schemaComparePublishProjectChanges(operationId, target.projectFilePath, target.folderStructure);
|
||||
|
||||
if (publishResult.success) {
|
||||
void vscode.window.showInformationMessage(constants.applySuccess);
|
||||
} else {
|
||||
void vscode.window.showErrorMessage(constants.applyError(publishResult.errorMessage));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a flat list of all files and folder under a folder.
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,610 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import * as mssql from '../../../mssql';
|
||||
import * as azdata from 'azdata';
|
||||
import * as constants from '../common/constants';
|
||||
import * as newProjectTool from '../tools/newProjectTool';
|
||||
import type * as mssqlVscode from 'vscode-mssql';
|
||||
|
||||
import { Deferred } from '../common/promise';
|
||||
import { Project } from '../models/project';
|
||||
import { cssStyles } from '../common/uiConstants';
|
||||
import { IconPathHelper } from '../common/iconHelper';
|
||||
import { UpdateProjectDataModel, UpdateProjectAction } from '../models/api/updateProject';
|
||||
import { exists, getAzdataApi, getDataWorkspaceExtensionApi } from '../common/utils';
|
||||
import * as path from 'path';
|
||||
|
||||
export class UpdateProjectFromDatabaseDialog {
|
||||
public dialog: azdata.window.Dialog;
|
||||
public serverDropdown: azdata.DropDownComponent | undefined;
|
||||
public databaseDropdown: azdata.DropDownComponent | undefined;
|
||||
public projectFileTextBox: azdata.InputBoxComponent | undefined;
|
||||
public compareActionRadioButton: azdata.RadioButtonComponent | undefined;
|
||||
private updateProjectFromDatabaseTab: azdata.window.DialogTab;
|
||||
private connectionButton: azdata.ButtonComponent | undefined;
|
||||
private folderStructureDropDown: azdata.DropDownComponent | undefined;
|
||||
private updateActionRadioButton: azdata.RadioButtonComponent | undefined;
|
||||
private formBuilder: azdata.FormBuilder | undefined;
|
||||
private connectionId: string | undefined;
|
||||
private profile: azdata.IConnectionProfile | undefined;
|
||||
public action: UpdateProjectAction | undefined;
|
||||
private toDispose: vscode.Disposable[] = [];
|
||||
private initDialogPromise: Deferred = new Deferred();
|
||||
public populatedInputsPromise: Deferred = new Deferred();
|
||||
|
||||
public updateProjectFromDatabaseCallback: ((model: UpdateProjectDataModel) => any) | undefined;
|
||||
|
||||
constructor(connection: azdata.IConnectionProfile | mssqlVscode.IConnectionInfo | undefined, private project: Project | undefined) {
|
||||
if (connection && 'connectionName' in connection) {
|
||||
this.profile = connection;
|
||||
}
|
||||
|
||||
// need to set profile when database is updated as well as here
|
||||
// see what schemaCompare is doing!
|
||||
|
||||
this.dialog = getAzdataApi()!.window.createModelViewDialog(constants.updateProjectFromDatabaseDialogName, 'updateProjectFromDatabaseDialog');
|
||||
this.updateProjectFromDatabaseTab = getAzdataApi()!.window.createTab(constants.updateProjectFromDatabaseDialogName);
|
||||
this.dialog.registerCloseValidator(async () => {
|
||||
return this.validate();
|
||||
});
|
||||
|
||||
this.toDispose.push(this.dialog.onClosed(_ => this.initDialogPromise.resolve()));
|
||||
}
|
||||
|
||||
public async openDialog(): Promise<void> {
|
||||
let connection = await azdata.connection.getCurrentConnection();
|
||||
if (connection) {
|
||||
this.connectionId = connection.connectionId;
|
||||
}
|
||||
|
||||
this.initializeDialog();
|
||||
|
||||
this.dialog.okButton.label = constants.updateText;
|
||||
this.dialog.okButton.enabled = false;
|
||||
this.toDispose.push(this.dialog.okButton.onClick(async () => await this.handleUpdateButtonClick()));
|
||||
|
||||
this.dialog.cancelButton.label = constants.cancelButtonText;
|
||||
|
||||
getAzdataApi()!.window.openDialog(this.dialog);
|
||||
await this.initDialogPromise;
|
||||
|
||||
this.tryEnableUpdateButton();
|
||||
}
|
||||
|
||||
private dispose(): void {
|
||||
this.toDispose.forEach(disposable => disposable.dispose());
|
||||
}
|
||||
|
||||
private initializeDialog(): void {
|
||||
this.initializeUpdateProjectFromDatabaseTab();
|
||||
this.dialog.content = [this.updateProjectFromDatabaseTab];
|
||||
}
|
||||
|
||||
private initializeUpdateProjectFromDatabaseTab(): void {
|
||||
this.updateProjectFromDatabaseTab.registerContent(async view => {
|
||||
|
||||
const connectionRow = this.createServerRow(view);
|
||||
const databaseRow = this.createDatabaseRow(view);
|
||||
await this.populateServerDropdown();
|
||||
|
||||
const sourceDatabaseFormSection = view.modelBuilder.flexContainer().withLayout({ flexFlow: 'column' }).component();
|
||||
sourceDatabaseFormSection.addItems([connectionRow, databaseRow]);
|
||||
|
||||
const projectLocationRow = this.createProjectLocationRow(view);
|
||||
const folderStructureRow = this.createFolderStructureRow(view);
|
||||
const targetProjectFormSection = view.modelBuilder.flexContainer().withLayout({ flexFlow: 'column' }).component();
|
||||
targetProjectFormSection.addItems([projectLocationRow, folderStructureRow]);
|
||||
|
||||
const actionRow = await this.createActionRow(view);
|
||||
const actionFormSection = view.modelBuilder.flexContainer().withLayout({ flexFlow: 'column' }).component();
|
||||
actionFormSection.addItems([actionRow]);
|
||||
|
||||
this.formBuilder = <azdata.FormBuilder>view.modelBuilder.formContainer()
|
||||
.withFormItems([
|
||||
{
|
||||
title: constants.sourceDatabase,
|
||||
components: [
|
||||
{
|
||||
component: sourceDatabaseFormSection,
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: constants.targetProject,
|
||||
components: [
|
||||
{
|
||||
component: targetProjectFormSection,
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: constants.updateAction,
|
||||
components: [
|
||||
{
|
||||
component: actionFormSection,
|
||||
}
|
||||
]
|
||||
}
|
||||
], {
|
||||
horizontal: false,
|
||||
titleFontSize: cssStyles.titleFontSize
|
||||
})
|
||||
.withLayout({
|
||||
width: '100%',
|
||||
padding: '10px 10px 0 20px'
|
||||
});
|
||||
|
||||
let formModel = this.formBuilder.component();
|
||||
await view.initializeModel(formModel);
|
||||
await this.connectionButton?.focus();
|
||||
this.initDialogPromise.resolve();
|
||||
});
|
||||
}
|
||||
|
||||
private createServerRow(view: azdata.ModelView): azdata.FlexContainer {
|
||||
this.createServerComponent(view);
|
||||
|
||||
const serverLabel = view.modelBuilder.text().withProps({
|
||||
value: constants.server,
|
||||
requiredIndicator: true,
|
||||
width: cssStyles.updateProjectFromDatabaseLabelWidth
|
||||
}).component();
|
||||
|
||||
const connectionRow = view.modelBuilder.flexContainer().withItems([serverLabel, this.serverDropdown!], { flex: '0 0 auto', CSSStyles: { 'margin-right': '10px', 'margin-bottom': '-5px', 'margin-top': '-10px' } }).withLayout({ flexFlow: 'row', alignItems: 'center' }).component();
|
||||
connectionRow.addItem(this.connectionButton!, { CSSStyles: { 'margin-right': '0px', 'margin-bottom': '-5px', 'margin-top': '-10px' } });
|
||||
|
||||
return connectionRow;
|
||||
}
|
||||
|
||||
private createDatabaseRow(view: azdata.ModelView): azdata.FlexContainer {
|
||||
this.createDatabaseComponent(view);
|
||||
|
||||
const databaseLabel = view.modelBuilder.text().withProps({
|
||||
value: constants.databaseNameLabel,
|
||||
requiredIndicator: true,
|
||||
width: cssStyles.updateProjectFromDatabaseLabelWidth
|
||||
}).component();
|
||||
|
||||
const databaseRow = view.modelBuilder.flexContainer().withItems([databaseLabel, <azdata.DropDownComponent>this.databaseDropdown!], { flex: '0 0 auto', CSSStyles: { 'margin-right': '10px', 'margin-bottom': '-10px' } }).withLayout({ flexFlow: 'row', alignItems: 'center' }).component();
|
||||
|
||||
return databaseRow;
|
||||
}
|
||||
|
||||
private createServerComponent(view: azdata.ModelView) {
|
||||
this.serverDropdown = view.modelBuilder.dropDown().withProps({
|
||||
editable: true,
|
||||
fireOnTextChange: true,
|
||||
width: cssStyles.updateProjectFromDatabaseTextboxWidth
|
||||
}).component();
|
||||
|
||||
this.createConnectionButton(view);
|
||||
|
||||
this.serverDropdown.onValueChanged(() => {
|
||||
this.tryEnableUpdateButton();
|
||||
});
|
||||
}
|
||||
|
||||
private createDatabaseComponent(view: azdata.ModelView) {
|
||||
this.databaseDropdown = view.modelBuilder.dropDown().withProps({
|
||||
editable: true,
|
||||
fireOnTextChange: true,
|
||||
width: cssStyles.updateProjectFromDatabaseTextboxWidth
|
||||
}).component();
|
||||
|
||||
this.databaseDropdown.onValueChanged(() => {
|
||||
this.tryEnableUpdateButton();
|
||||
});
|
||||
}
|
||||
|
||||
private async populateServerDropdown() {
|
||||
this.serverDropdown!.loading = true;
|
||||
const values = await this.getServerValues();
|
||||
|
||||
if (values && values.length > 0) {
|
||||
await this.serverDropdown!.updateProperties({
|
||||
values: values,
|
||||
value: values[0]
|
||||
});
|
||||
}
|
||||
|
||||
this.serverDropdown!.loading = false;
|
||||
|
||||
if (this.serverDropdown!.value) {
|
||||
await this.populateDatabaseDropdown();
|
||||
}
|
||||
|
||||
this.tryEnableUpdateButton();
|
||||
this.populatedInputsPromise.resolve();
|
||||
}
|
||||
|
||||
protected async populateDatabaseDropdown() {
|
||||
const connectionProfile = (this.serverDropdown!.value as ConnectionDropdownValue).connection;
|
||||
|
||||
this.databaseDropdown!.loading = true;
|
||||
|
||||
await this.databaseDropdown!.updateProperties({
|
||||
values: [],
|
||||
value: undefined
|
||||
});
|
||||
|
||||
let values = [];
|
||||
try {
|
||||
values = await this.getDatabaseValues(connectionProfile.connectionId);
|
||||
} catch (e) {
|
||||
// if the user doesn't have access to master, just set the database of the connection profile
|
||||
values = [connectionProfile.databaseName];
|
||||
console.warn(e);
|
||||
}
|
||||
|
||||
if (values && values.length > 0) {
|
||||
await this.databaseDropdown!.updateProperties({
|
||||
values: values,
|
||||
value: values[0],
|
||||
});
|
||||
}
|
||||
|
||||
this.databaseDropdown!.loading = false;
|
||||
}
|
||||
|
||||
private async getServerValues() {
|
||||
let cons = await azdata.connection.getConnections(/* activeConnectionsOnly */ true);
|
||||
|
||||
// This user has no active connections
|
||||
if (!cons || cons.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Update connection icon to "connected" state
|
||||
this.connectionButton!.iconPath = IconPathHelper.connect;
|
||||
|
||||
// reverse list so that most recent connections are first
|
||||
cons.reverse();
|
||||
|
||||
let count = -1;
|
||||
let idx = -1;
|
||||
let values = cons.map(c => {
|
||||
count++;
|
||||
|
||||
let usr = c.options.user;
|
||||
|
||||
if (!usr) {
|
||||
usr = constants.defaultUser;
|
||||
}
|
||||
|
||||
let srv = c.options.server;
|
||||
|
||||
let finalName = `${srv} (${usr})`;
|
||||
|
||||
if (c.options.connectionName) {
|
||||
finalName = c.options.connectionName;
|
||||
}
|
||||
|
||||
if (c.connectionId === this.connectionId) {
|
||||
idx = count;
|
||||
}
|
||||
|
||||
return {
|
||||
connection: c,
|
||||
displayName: finalName,
|
||||
name: srv,
|
||||
};
|
||||
});
|
||||
|
||||
// move server of current connection to the top of the list so it is the default
|
||||
if (idx >= 1) {
|
||||
let tmp = values[0];
|
||||
values[0] = values[idx];
|
||||
values[idx] = tmp;
|
||||
}
|
||||
|
||||
values = values.reduce((uniqueValues: { connection: azdata.connection.ConnectionProfile, displayName: string, name: string }[], conn) => {
|
||||
let exists = uniqueValues.find(x => x.displayName === conn.displayName);
|
||||
if (!exists) {
|
||||
uniqueValues.push(conn);
|
||||
}
|
||||
return uniqueValues;
|
||||
}, []);
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
protected async getDatabaseValues(connectionId: string) {
|
||||
let idx = -1;
|
||||
let count = -1;
|
||||
|
||||
let values = (await azdata.connection.listDatabases(connectionId)).sort((a, b) => a.localeCompare(b)).map(db => {
|
||||
count++;
|
||||
|
||||
// put currently selected db at the top of the dropdown if there is one
|
||||
if (this.profile && this.profile.databaseName && this.profile.databaseName === db) {
|
||||
idx = count;
|
||||
}
|
||||
|
||||
return db;
|
||||
});
|
||||
|
||||
if (idx >= 0) {
|
||||
let tmp = values[0];
|
||||
values[0] = values[idx];
|
||||
values[idx] = tmp;
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
private createConnectionButton(view: azdata.ModelView) {
|
||||
this.connectionButton = view.modelBuilder.button().withProps({
|
||||
ariaLabel: constants.selectConnection,
|
||||
iconPath: IconPathHelper.selectConnection,
|
||||
height: '20px',
|
||||
width: '20px'
|
||||
}).component();
|
||||
|
||||
this.connectionButton.onDidClick(async () => {
|
||||
await this.connectionButtonClick();
|
||||
this.connectionButton!.iconPath = IconPathHelper.connect;
|
||||
});
|
||||
}
|
||||
|
||||
private async connectionButtonClick() {
|
||||
let connection = await azdata.connection.openConnectionDialog();
|
||||
if (connection) {
|
||||
this.connectionId = connection.connectionId;
|
||||
await this.populateServerDropdown();
|
||||
}
|
||||
}
|
||||
|
||||
private createProjectLocationRow(view: azdata.ModelView): azdata.FlexContainer {
|
||||
const browseFolderButton: azdata.Component = this.createBrowseFileButton(view);
|
||||
|
||||
const value = this.project ? this.project.projectFilePath : '';
|
||||
|
||||
this.projectFileTextBox = view.modelBuilder.inputBox().withProps({
|
||||
value: value,
|
||||
ariaLabel: constants.projectLocationLabel,
|
||||
placeHolder: constants.projectToUpdatePlaceholderText,
|
||||
width: cssStyles.updateProjectFromDatabaseTextboxWidth
|
||||
}).component();
|
||||
|
||||
this.projectFileTextBox.onTextChanged(async () => {
|
||||
await this.projectFileTextBox!.updateProperty('title', this.projectFileTextBox!.value);
|
||||
this.tryEnableUpdateButton();
|
||||
});
|
||||
|
||||
const projectLocationLabel = view.modelBuilder.text().withProps({
|
||||
value: constants.projectLocationLabel,
|
||||
requiredIndicator: true,
|
||||
width: cssStyles.updateProjectFromDatabaseLabelWidth
|
||||
}).component();
|
||||
|
||||
const projectLocationRow = view.modelBuilder.flexContainer().withItems([projectLocationLabel, this.projectFileTextBox], { flex: '0 0 auto', CSSStyles: { 'margin-right': '10px', 'margin-bottom': '-5px', 'margin-top': '-10px' } }).component();
|
||||
projectLocationRow.addItem(browseFolderButton, { CSSStyles: { 'margin-right': '0px', 'margin-bottom': '-5px', 'margin-top': '-10px' } });
|
||||
|
||||
return projectLocationRow;
|
||||
}
|
||||
|
||||
private createBrowseFileButton(view: azdata.ModelView): azdata.ButtonComponent {
|
||||
const browseFolderButton = view.modelBuilder.button().withProps({
|
||||
ariaLabel: constants.browseButtonText,
|
||||
iconPath: IconPathHelper.folder_blue,
|
||||
height: '18px',
|
||||
width: '18px'
|
||||
}).component();
|
||||
|
||||
browseFolderButton.onDidClick(async () => {
|
||||
let fileUris = await vscode.window.showOpenDialog({
|
||||
canSelectFiles: true,
|
||||
canSelectFolders: false,
|
||||
canSelectMany: false,
|
||||
openLabel: constants.selectString,
|
||||
defaultUri: newProjectTool.defaultProjectSaveLocation(),
|
||||
filters: {
|
||||
'Files': ['sqlproj']
|
||||
}
|
||||
});
|
||||
|
||||
if (!fileUris || fileUris.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.projectFileTextBox!.value = fileUris[0].fsPath;
|
||||
await this.projectFileTextBox!.updateProperty('title', fileUris[0].fsPath);
|
||||
});
|
||||
|
||||
return browseFolderButton;
|
||||
}
|
||||
|
||||
private createFolderStructureRow(view: azdata.ModelView): azdata.FlexContainer {
|
||||
this.folderStructureDropDown = view.modelBuilder.dropDown().withProps({
|
||||
values: [constants.file, constants.flat, constants.objectType, constants.schema, constants.schemaObjectType],
|
||||
value: constants.schemaObjectType,
|
||||
ariaLabel: constants.folderStructureLabel,
|
||||
required: true,
|
||||
width: cssStyles.updateProjectFromDatabaseTextboxWidth
|
||||
}).component();
|
||||
|
||||
this.folderStructureDropDown.onValueChanged(() => {
|
||||
this.tryEnableUpdateButton();
|
||||
});
|
||||
|
||||
const folderStructureLabel = view.modelBuilder.text().withProps({
|
||||
value: constants.folderStructureLabel,
|
||||
requiredIndicator: true,
|
||||
width: cssStyles.createProjectFromDatabaseLabelWidth
|
||||
}).component();
|
||||
|
||||
const folderStructureRow = view.modelBuilder.flexContainer().withItems([folderStructureLabel, <azdata.DropDownComponent>this.folderStructureDropDown], { flex: '0 0 auto', CSSStyles: { 'margin-right': '10px', 'margin-bottom': '-10px' } }).withLayout({ flexFlow: 'row', alignItems: 'center' }).component();
|
||||
|
||||
return folderStructureRow;
|
||||
}
|
||||
|
||||
private async createActionRow(view: azdata.ModelView): Promise<azdata.FlexContainer> {
|
||||
this.compareActionRadioButton = view.modelBuilder.radioButton().withProps({
|
||||
name: 'action',
|
||||
label: constants.compareActionRadioButtonLabel,
|
||||
checked: true
|
||||
}).component();
|
||||
|
||||
this.updateActionRadioButton = view.modelBuilder.radioButton().withProps({
|
||||
name: 'action',
|
||||
label: constants.updateActionRadioButtonLabel
|
||||
}).component();
|
||||
|
||||
await this.compareActionRadioButton.updateProperties({ checked: true });
|
||||
this.action = UpdateProjectAction.Compare;
|
||||
|
||||
this.compareActionRadioButton.onDidClick(async () => {
|
||||
this.action = UpdateProjectAction.Compare;
|
||||
this.tryEnableUpdateButton();
|
||||
});
|
||||
|
||||
this.updateActionRadioButton.onDidClick(async () => {
|
||||
this.action = UpdateProjectAction.Update;
|
||||
this.tryEnableUpdateButton();
|
||||
});
|
||||
|
||||
let radioButtons = view.modelBuilder.flexContainer()
|
||||
.withLayout({ flexFlow: 'column' })
|
||||
.withItems([this.compareActionRadioButton, this.updateActionRadioButton])
|
||||
.withProps({ ariaRole: 'radiogroup' })
|
||||
.component();
|
||||
|
||||
const actionLabel = view.modelBuilder.text().withProps({
|
||||
value: constants.actionLabel,
|
||||
requiredIndicator: true,
|
||||
width: cssStyles.updateProjectFromDatabaseLabelWidth
|
||||
}).component();
|
||||
|
||||
const actionRow = view.modelBuilder.flexContainer().withItems([actionLabel, <azdata.FlexContainer>radioButtons], { flex: '0 0 auto', CSSStyles: { 'margin-right': '10px', 'margin-bottom': '-10px' } }).withLayout({ flexFlow: 'row', alignItems: 'center' }).component();
|
||||
|
||||
return actionRow;
|
||||
}
|
||||
|
||||
// only enable Update button if all fields are filled
|
||||
public tryEnableUpdateButton(): void {
|
||||
if (this.serverDropdown?.value
|
||||
&& this.databaseDropdown?.value
|
||||
&& this.projectFileTextBox?.value
|
||||
&& this.folderStructureDropDown?.value
|
||||
&& this.action !== undefined) {
|
||||
this.dialog.okButton.enabled = true;
|
||||
} else {
|
||||
this.dialog.okButton.enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
public async handleUpdateButtonClick(): Promise<void> {
|
||||
const serverDropdownValue = this.serverDropdown!.value! as azdata.CategoryValue as ConnectionDropdownValue;
|
||||
const ownerUri = await azdata.connection.getUriForConnection(serverDropdownValue.connection.connectionId);
|
||||
|
||||
let connection = (await azdata.connection.getConnections(true)).filter(con => con.connectionId === serverDropdownValue.connection.connectionId)[0];
|
||||
connection.databaseName = this.databaseDropdown!.value! as string;
|
||||
|
||||
const credentials = await azdata.connection.getCredentials(connection.connectionId);
|
||||
if (credentials.hasOwnProperty('password')) {
|
||||
connection.password = connection.options.password = credentials.password;
|
||||
}
|
||||
|
||||
const connectionDetails: azdata.IConnectionProfile = {
|
||||
id: connection.connectionId,
|
||||
userName: connection.userName,
|
||||
password: connection.password,
|
||||
serverName: connection.serverName,
|
||||
databaseName: connection.databaseName,
|
||||
connectionName: connection.connectionName,
|
||||
providerName: connection.providerId,
|
||||
groupId: connection.groupId,
|
||||
groupFullName: connection.groupFullName,
|
||||
authenticationType: connection.authenticationType,
|
||||
savePassword: connection.savePassword,
|
||||
saveProfile: connection.saveProfile,
|
||||
options: connection.options,
|
||||
};
|
||||
|
||||
const sourceEndpointInfo: mssql.SchemaCompareEndpointInfo = {
|
||||
endpointType: mssql.SchemaCompareEndpointType.Database,
|
||||
databaseName: this.databaseDropdown!.value! as string,
|
||||
serverDisplayName: serverDropdownValue.displayName,
|
||||
serverName: serverDropdownValue.name!,
|
||||
connectionDetails: connectionDetails,
|
||||
ownerUri: ownerUri,
|
||||
projectFilePath: '',
|
||||
folderStructure: '',
|
||||
targetScripts: [],
|
||||
dataSchemaProvider: '',
|
||||
packageFilePath: '',
|
||||
connectionName: serverDropdownValue.connection.options.connectionName
|
||||
};
|
||||
|
||||
const targetEndpointInfo: mssql.SchemaCompareEndpointInfo = {
|
||||
endpointType: mssql.SchemaCompareEndpointType.Project,
|
||||
projectFilePath: this.projectFileTextBox!.value!,
|
||||
folderStructure: this.folderStructureDropDown!.value as string,
|
||||
targetScripts: [],
|
||||
dataSchemaProvider: '',
|
||||
connectionDetails: connectionDetails,
|
||||
databaseName: '',
|
||||
serverDisplayName: '',
|
||||
serverName: '',
|
||||
ownerUri: '',
|
||||
packageFilePath: '',
|
||||
};
|
||||
|
||||
const model: UpdateProjectDataModel = {
|
||||
sourceEndpointInfo: sourceEndpointInfo,
|
||||
targetEndpointInfo: targetEndpointInfo,
|
||||
action: this.action!
|
||||
};
|
||||
|
||||
getAzdataApi()!.window.closeDialog(this.dialog);
|
||||
await this.updateProjectFromDatabaseCallback!(model);
|
||||
|
||||
this.dispose();
|
||||
}
|
||||
|
||||
async validate(): Promise<boolean> {
|
||||
try {
|
||||
if (await getDataWorkspaceExtensionApi().validateWorkspace() === false) {
|
||||
return false;
|
||||
}
|
||||
// the selected location should be an existing directory
|
||||
const parentDirectoryExists = await exists(path.dirname(this.projectFileTextBox!.value!));
|
||||
if (!parentDirectoryExists) {
|
||||
this.showErrorMessage(constants.ProjectParentDirectoryNotExistError(this.projectFileTextBox!.value!));
|
||||
return false;
|
||||
}
|
||||
|
||||
// the selected location must contain a .sqlproj file
|
||||
const fileExists = await exists(this.projectFileTextBox!.value!);
|
||||
if (!fileExists) {
|
||||
this.showErrorMessage(constants.noSqlProjFile);
|
||||
return false;
|
||||
}
|
||||
|
||||
// schema compare extension must be downloaded
|
||||
if (!vscode.extensions.getExtension(constants.schemaCompareExtensionId)) {
|
||||
this.showErrorMessage(constants.noSchemaCompareExtension);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (err) {
|
||||
this.showErrorMessage(err?.message ? err.message : err);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
protected showErrorMessage(message: string): void {
|
||||
this.dialog.message = {
|
||||
text: message,
|
||||
level: getAzdataApi()!.window.MessageLevel.Error
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export interface ConnectionDropdownValue extends azdata.CategoryValue {
|
||||
connection: azdata.connection.ConnectionProfile;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as mssql from '../../../../mssql/src/mssql';
|
||||
|
||||
export interface UpdateProjectDataModel {
|
||||
sourceEndpointInfo: mssql.SchemaCompareEndpointInfo;
|
||||
targetEndpointInfo: mssql.SchemaCompareEndpointInfo;
|
||||
action: UpdateProjectAction;
|
||||
}
|
||||
|
||||
export const enum UpdateProjectAction {
|
||||
Compare = 0,
|
||||
Update = 1
|
||||
}
|
||||
@@ -168,4 +168,19 @@ export class SqlDatabaseProjectProvider implements dataworkspace.IProjectProvide
|
||||
const projectUri = getDataWorkspaceExtensionApi().openSpecificProjectNewProjectDialog(projectType);
|
||||
return projectUri;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the list of .sql scripts contained in a project
|
||||
* @param projectFilePath
|
||||
*/
|
||||
async getProjectScriptFiles(projectFilePath: string): Promise<string[]> {
|
||||
return await this.projectController.getProjectScriptFiles(projectFilePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the Database Schema Provider version for a SQL project
|
||||
*/
|
||||
async getProjectDatabaseSchemaProvider(projectFilePath: string): Promise<string> {
|
||||
return await this.projectController.getProjectDatabaseSchemaProvider(projectFilePath);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,17 @@ declare module 'sqldbproj' {
|
||||
* @returns uri of the created the project or undefined if no project was created
|
||||
*/
|
||||
openSqlNewProjectDialog(allowedTargetPlatforms?: SqlTargetPlatform[]): Promise<vscode.Uri | undefined>;
|
||||
|
||||
/**
|
||||
* Gets the list of .sql scripts contained in a project
|
||||
* @param projectFilePath
|
||||
*/
|
||||
getProjectScriptFiles(projectFilePath: string): Promise<string[]>;
|
||||
|
||||
/**
|
||||
* Gets the Database Schema Provider version for a SQL project
|
||||
*/
|
||||
getProjectDatabaseSchemaProvider(projectFilePath: string): Promise<string>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as azdata from 'azdata';
|
||||
import * as should from 'should';
|
||||
import * as sinon from 'sinon';
|
||||
import * as baselines from '../baselines/baselines';
|
||||
import * as testUtils from '../testUtils';
|
||||
|
||||
import { UpdateProjectFromDatabaseDialog } from '../../dialogs/updateProjectFromDatabaseDialog';
|
||||
import { mockConnectionProfile } from '../testContext';
|
||||
|
||||
describe('Update Project From Database Dialog', () => {
|
||||
before(async function (): Promise<void> {
|
||||
await baselines.loadBaselines();
|
||||
});
|
||||
|
||||
afterEach(function (): void {
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
it('Should populate endpoints correctly when no context passed', async function (): Promise<void> {
|
||||
const dialog = new UpdateProjectFromDatabaseDialog(undefined, undefined);
|
||||
await dialog.openDialog();
|
||||
|
||||
should.equal(dialog.serverDropdown!.value, undefined, `Server dropdown should not be populated, but instead was "${dialog.serverDropdown!.value}".`);
|
||||
should.equal(dialog.databaseDropdown!.value, undefined, `Database dropdown should not be populated, but instead was "${dialog.databaseDropdown!.value}".`);
|
||||
should.equal(dialog.projectFileTextBox!.value, '', `Project file textbox should not be populated, but instead was "${dialog.projectFileTextBox!.value}".`);
|
||||
should.equal(dialog.dialog.okButton.enabled, false, 'Okay button should be disabled.');
|
||||
});
|
||||
|
||||
it('Should populate endpoints correctly when Project context is passed', async function (): Promise<void> {
|
||||
const project = await testUtils.createTestProject(baselines.openProjectFileBaseline);
|
||||
const dialog = new UpdateProjectFromDatabaseDialog(undefined, project);
|
||||
await dialog.openDialog();
|
||||
|
||||
should.equal(dialog.serverDropdown!.value, undefined, `Server dropdown should not be populated, but instead was "${dialog.serverDropdown!.value}".`);
|
||||
should.equal(dialog.databaseDropdown!.value, undefined, `Database dropdown should not be populated, but instead was "${dialog.databaseDropdown!.value}".`);
|
||||
should.equal(dialog.projectFileTextBox!.value, project.projectFilePath, `Project file textbox should be the sqlproj path (${project.projectFilePath}), but instead was "${dialog.projectFileTextBox!.value}".`);
|
||||
should.equal(dialog.dialog.okButton.enabled, false, 'Okay button should be disabled.');
|
||||
});
|
||||
|
||||
it('Should populate endpoints correctly when Connection context is passed', async function (): Promise<void> {
|
||||
sinon.stub(azdata.connection, 'getConnections').resolves([<azdata.connection.ConnectionProfile><unknown>mockConnectionProfile]);
|
||||
sinon.stub(azdata.connection, 'listDatabases').resolves([mockConnectionProfile.databaseName!]);
|
||||
|
||||
const profile = mockConnectionProfile;
|
||||
const dialog = new UpdateProjectFromDatabaseDialog(profile, undefined);
|
||||
await dialog.openDialog();
|
||||
await dialog.populatedInputsPromise;
|
||||
|
||||
should.equal((<any>dialog.serverDropdown!.value).displayName, profile.options['connectionName'], `Server dropdown should be "${profile.options['connectionName']}", but instead was "${(<any>dialog.serverDropdown!.value).displayName}".`);
|
||||
should.equal(dialog.databaseDropdown!.value, profile.databaseName, `Database dropdown should be "${profile.databaseName}", but instead was "${dialog.databaseDropdown!.value}".`);
|
||||
should.equal(dialog.projectFileTextBox!.value, '', `Project file textbox should not be populated, but instead was "${dialog.projectFileTextBox!.value}".`);
|
||||
should.equal(dialog.dialog.okButton.enabled, false, 'Okay button should be disabled.');
|
||||
});
|
||||
|
||||
it('Should populate endpoints correctly when context is complete', async function (): Promise<void> {
|
||||
const project = await testUtils.createTestProject(baselines.openProjectFileBaseline);
|
||||
sinon.stub(azdata.connection, 'getConnections').resolves([<azdata.connection.ConnectionProfile><unknown>mockConnectionProfile]);
|
||||
sinon.stub(azdata.connection, 'listDatabases').resolves([mockConnectionProfile.databaseName!]);
|
||||
|
||||
const profile = mockConnectionProfile;
|
||||
const dialog = new UpdateProjectFromDatabaseDialog(profile, project);
|
||||
await dialog.openDialog();
|
||||
await dialog.populatedInputsPromise;
|
||||
|
||||
should.equal((<any>dialog.serverDropdown!.value).displayName, profile.options['connectionName'], `Server dropdown should be "${profile.options['connectionName']}", but instead was "${(<any>dialog.serverDropdown!.value).displayName}".`);
|
||||
should.equal(dialog.databaseDropdown!.value, profile.databaseName, `Database dropdown should as "${profile.databaseName}", but instead was "${dialog.databaseDropdown!.value}".`);
|
||||
should.equal(dialog.projectFileTextBox!.value, project.projectFilePath, `Project file textbox should be the sqlproj path (${project.projectFilePath}), but instead was "${dialog.projectFileTextBox!.value}".`);
|
||||
should.equal(dialog.dialog.okButton.enabled, true, 'Okay button should be enabled when dialog is complete.');
|
||||
});
|
||||
});
|
||||
@@ -201,6 +201,7 @@ export const mockConnectionProfile: azdata.IConnectionProfile = {
|
||||
database: 'My Database',
|
||||
user: 'My User',
|
||||
password: 'My Pwd',
|
||||
authenticationType: 'SqlLogin'
|
||||
authenticationType: 'SqlLogin',
|
||||
connectionName: 'My Connection Name'
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user