From 244d56eb1204f22f19d8b1a785e87f91394d7f85 Mon Sep 17 00:00:00 2001 From: Sakshi Sharma <57200045+SakshiS-harma@users.noreply.github.com> Date: Tue, 16 May 2023 10:47:58 -0700 Subject: [PATCH] Add option to save Publish profile in VScode (#23067) * Publish profile save changes for VSCode * Fix publish settings * Fix publish settings * Address comments * Address comments * Address comments * Address comment --- .../src/common/constants.ts | 1 + .../src/controllers/projectController.ts | 7 +- .../src/dialogs/publishDatabaseDialog.ts | 21 +- .../src/dialogs/publishDatabaseQuickpick.ts | 5 +- .../src/models/deploy/publishSettings.ts | 3 +- .../models/publishProfile/publishProfile.ts | 182 +++++++++++++++++- .../dialogs/publishDatabaseDialog.test.ts | 9 +- 7 files changed, 199 insertions(+), 29 deletions(-) diff --git a/extensions/sql-database-projects/src/common/constants.ts b/extensions/sql-database-projects/src/common/constants.ts index 8373d9bd89..969e6bde7d 100644 --- a/extensions/sql-database-projects/src/common/constants.ts +++ b/extensions/sql-database-projects/src/common/constants.ts @@ -167,6 +167,7 @@ export const selectDatabase = localize('selectDatabase', "Select database"); export const done = localize('done', "Done"); export const nameMustNotBeEmpty = localize('nameMustNotBeEmpty', "Name must not be empty"); export const versionMustNotBeEmpty = localize('versionMustNotBeEmpty', "Version must not be empty"); +export const saveProfile = localize('saveProfile', "Would you like to save the settings in a profile (.publish.xml)?"); //#endregion diff --git a/extensions/sql-database-projects/src/controllers/projectController.ts b/extensions/sql-database-projects/src/controllers/projectController.ts index ed538b00f5..43b0a257b6 100644 --- a/extensions/sql-database-projects/src/controllers/projectController.ts +++ b/extensions/sql-database-projects/src/controllers/projectController.ts @@ -25,7 +25,7 @@ import { ImportDataModel } from '../models/api/import'; import { NetCoreTool, DotNetError } from '../tools/netcoreTool'; import { ShellCommandOptions } from '../tools/shellExecutionHelper'; import { BuildHelper } from '../tools/buildHelper'; -import { readPublishProfile, savePublishProfile } from '../models/publishProfile/publishProfile'; +import { readPublishProfile, promptForSavingProfile, savePublishProfile } from '../models/publishProfile/publishProfile'; import { AddDatabaseReferenceDialog } from '../dialogs/addDatabaseReferenceDialog'; import { ISystemDatabaseReferenceSettings, IDacpacReferenceSettings, IProjectReferenceSettings, INugetPackageReferenceSettings } from '../models/IDatabaseReferenceSettings'; import { DatabaseReferenceTreeItem } from '../models/tree/databaseReferencesTreeItem'; @@ -474,6 +474,7 @@ export class ProjectsController { if (publishTarget === constants.PublishTargetType.docker) { const publishToDockerSettings = await getPublishToDockerSettings(project); + void promptForSavingProfile(project, publishToDockerSettings); // not awaiting this call, because saving profile should not stop the actual publish workflow if (!publishToDockerSettings) { // User cancelled return; @@ -482,6 +483,7 @@ export class ProjectsController { } else if (publishTarget === constants.PublishTargetType.newAzureServer) { try { const settings = await launchCreateAzureServerQuickPick(project, this.azureSqlClient); + void promptForSavingProfile(project, settings); // not awaiting this call, because saving profile should not stop the actual publish workflow if (settings?.deploySettings && settings?.sqlDbSetting) { await this.publishToNewAzureServer(project, settings); } @@ -492,6 +494,7 @@ export class ProjectsController { } else { let settings: ISqlProjectPublishSettings | undefined = await getPublishDatabaseSettings(project); + void promptForSavingProfile(project, settings); // not awaiting this call, because saving profile should not stop the actual publish workflow if (settings) { // 5. Select action to take const action = await vscode.window.showQuickPick( @@ -537,7 +540,7 @@ export class ProjectsController { const dacFxService = await utils.getDacFxService(); let result: mssql.DacFxResult; - telemetryProps.profileUsed = (settings.profileUsed ?? false).toString(); + telemetryProps.profileUsed = (settings.publishProfileUri !== undefined ? true : false).toString(); const currentDate = new Date(); const actionStartTime = currentDate.getTime(); const currentPublishTimeInfo = `${currentDate.toLocaleDateString()} ${constants.at} ${currentDate.toLocaleTimeString()}`; diff --git a/extensions/sql-database-projects/src/dialogs/publishDatabaseDialog.ts b/extensions/sql-database-projects/src/dialogs/publishDatabaseDialog.ts index b59654280e..aa0c205149 100644 --- a/extensions/sql-database-projects/src/dialogs/publishDatabaseDialog.ts +++ b/extensions/sql-database-projects/src/dialogs/publishDatabaseDialog.ts @@ -20,7 +20,7 @@ import { TelemetryActions, TelemetryReporter, TelemetryViews } from '../common/t import { Deferred } from '../common/promise'; import { PublishOptionsDialog } from './publishOptionsDialog'; import { IPublishToDockerSettings, ISqlProjectPublishSettings } from '../models/deploy/publishSettings'; -import { PublishProfile } from '../models/publishProfile/publishProfile'; +import { PublishProfile, promptToSaveProfile } from '../models/publishProfile/publishProfile'; interface DataSourceDropdownValue extends azdataType.CategoryValue { dataSource: SqlConnectionDataSource; @@ -56,7 +56,6 @@ export class PublishDatabaseDialog { private connectionIsDataSource: boolean | undefined; private sqlCmdVars: Map | undefined; private deploymentOptions: DeploymentOptions | undefined; - private profileUsed: boolean = false; private serverName: string | undefined; protected optionsButton: azdataType.ButtonComponent | undefined; private publishOptionsDialog: PublishOptionsDialog | undefined; @@ -240,7 +239,7 @@ export class PublishDatabaseDialog { connectionUri: await this.getConnectionUri(), sqlCmdVariables: this.getSqlCmdVariablesForPublish(), deploymentOptions: await this.getDeploymentOptions(), - profileUsed: this.profileUsed + publishProfileUri: this.publishProfileUri }; utils.getAzdataApi()!.window.closeDialog(this.dialog); @@ -273,7 +272,7 @@ export class PublishDatabaseDialog { connectionUri: '', sqlCmdVariables: this.getSqlCmdVariablesForPublish(), deploymentOptions: await this.getDeploymentOptions(), - profileUsed: this.profileUsed + publishProfileUri: this.publishProfileUri } }; @@ -294,7 +293,7 @@ export class PublishDatabaseDialog { connectionUri: await this.getConnectionUri(), sqlCmdVariables: sqlCmdVars, deploymentOptions: await this.getDeploymentOptions(), - profileUsed: this.profileUsed + publishProfileUri: this.publishProfileUri }; utils.getAzdataApi()!.window.closeDialog(this.dialog); @@ -831,7 +830,6 @@ export class PublishDatabaseDialog { this.loadProfileTextBox!.value = fileUris[0].fsPath; await this.loadProfileTextBox!.updateProperty('title', fileUris[0].fsPath); - this.profileUsed = true; this.publishProfileUri = fileUris[0]; } }); @@ -850,15 +848,7 @@ export class PublishDatabaseDialog { }).component(); saveProfileAsButton.onDidClick(async () => { - const filePath = await vscode.window.showSaveDialog( - { - defaultUri: this.publishProfileUri ?? vscode.Uri.file(path.join(this.project.projectFolderPath, `${this.project.projectFileName}_1.publish.xml`)), - saveLabel: constants.save, - filters: { - 'Publish Settings Files': ['publish.xml'], - } - } - ); + const filePath = await promptToSaveProfile(this.project, this.publishProfileUri); if (!filePath) { return; @@ -873,7 +863,6 @@ export class PublishDatabaseDialog { TelemetryReporter.sendActionEvent(TelemetryViews.SqlProjectPublishDialog, TelemetryActions.profileSaved); } - this.profileUsed = true; this.publishProfileUri = filePath; await this.project.addNoneItem(path.relative(this.project.projectFolderPath, filePath.fsPath)); diff --git a/extensions/sql-database-projects/src/dialogs/publishDatabaseQuickpick.ts b/extensions/sql-database-projects/src/dialogs/publishDatabaseQuickpick.ts index e7b4d0d9e8..47186080a2 100644 --- a/extensions/sql-database-projects/src/dialogs/publishDatabaseQuickpick.ts +++ b/extensions/sql-database-projects/src/dialogs/publishDatabaseQuickpick.ts @@ -21,6 +21,7 @@ import { ISqlProjectPublishSettings } from '../models/deploy/publishSettings'; export async function getPublishDatabaseSettings(project: ISqlProject, promptForConnection: boolean = true): Promise { // 1. Select publish settings file (optional) + let publishProfileUri; // Create custom quickpick so we can control stuff like displaying the loading indicator const quickPick = vscode.window.createQuickPick(); quickPick.items = [{ label: constants.dontUseProfile }, { label: constants.browseForProfileWithIcon }]; @@ -42,7 +43,7 @@ export async function getPublishDatabaseSettings(project: ISqlProject, promptFor // If the user cancels out of the file picker then just return and let them choose another option return; } - let publishProfileUri = locations[0]; + publishProfileUri = locations[0]; try { // Show loading state while reading profile quickPick.busy = true; @@ -214,7 +215,7 @@ export async function getPublishDatabaseSettings(project: ISqlProject, promptFor connectionUri: connectionUri || '', sqlCmdVariables: sqlCmdVariables, deploymentOptions: publishProfile?.options ?? await getDefaultPublishDeploymentOptions(project), - profileUsed: !!publishProfile + publishProfileUri: publishProfileUri }; return settings; } diff --git a/extensions/sql-database-projects/src/models/deploy/publishSettings.ts b/extensions/sql-database-projects/src/models/deploy/publishSettings.ts index 181f4b4652..d50bb32e3e 100644 --- a/extensions/sql-database-projects/src/models/deploy/publishSettings.ts +++ b/extensions/sql-database-projects/src/models/deploy/publishSettings.ts @@ -3,6 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as vscode from 'vscode'; import { ISqlConnectionProperties } from 'sqldbproj'; import { DeploymentOptions as mssqlDeploymentOptions } from 'mssql'; import { DeploymentOptions as vscodeMssqlDeploymentOptions } from 'vscode-mssql'; @@ -18,7 +19,7 @@ export interface ISqlProjectPublishSettings { connectionUri: string; sqlCmdVariables?: Map; deploymentOptions?: DeploymentOptions; - profileUsed?: boolean; + publishProfileUri?: vscode.Uri; } /** diff --git a/extensions/sql-database-projects/src/models/publishProfile/publishProfile.ts b/extensions/sql-database-projects/src/models/publishProfile/publishProfile.ts index 9e5f8e17ff..c3c16c8bdd 100644 --- a/extensions/sql-database-projects/src/models/publishProfile/publishProfile.ts +++ b/extensions/sql-database-projects/src/models/publishProfile/publishProfile.ts @@ -9,10 +9,14 @@ import * as utils from '../../common/utils'; import * as mssql from 'mssql'; import * as vscodeMssql from 'vscode-mssql'; import * as vscode from 'vscode'; +import * as path from 'path'; import { promises as fs } from 'fs'; import { SqlConnectionDataSource } from '../dataSources/sqlConnectionStringSource'; import { TelemetryActions, TelemetryReporter, TelemetryViews } from '../../common/telemetry'; +import { Project } from '../project'; +import { IPublishToDockerSettings, ISqlProjectPublishSettings } from '../deploy/publishSettings'; +import { ISqlDbDeployProfile } from '../deploy/deployProfile'; // only reading db name, connection string, and SQLCMD vars from profile for now export interface PublishProfile { @@ -129,7 +133,181 @@ async function readConnectionString(xmlDoc: any): Promise<{ connectionId: string /** * saves publish settings to the specified profile file */ -export async function savePublishProfile(profilePath: string, databaseName: string, connectionString: string, sqlCommandVariableValues?: Map, deploymentOptions?: mssql.DeploymentOptions): Promise { +export async function savePublishProfile(profilePath: string, databaseName: string, connectionString: string, sqlCommandVariableValues?: Map, deploymentOptions?: mssql.DeploymentOptions | vscodeMssql.DeploymentOptions): Promise { const dacFxService = await utils.getDacFxService(); - await dacFxService.savePublishProfile(profilePath, databaseName, connectionString, sqlCommandVariableValues, deploymentOptions); + if (utils.getAzdataApi()) { + await (dacFxService as mssql.IDacFxService).savePublishProfile(profilePath, databaseName, connectionString, sqlCommandVariableValues, deploymentOptions as mssql.DeploymentOptions); + } else { + await (dacFxService as vscodeMssql.IDacFxService).savePublishProfile(profilePath, databaseName, connectionString, sqlCommandVariableValues, deploymentOptions as vscodeMssql.DeploymentOptions); + } +} + +export function promptToSaveProfile(project: Project, publishProfileUri?: vscode.Uri) { + return vscode.window.showSaveDialog( + { + defaultUri: publishProfileUri ?? vscode.Uri.file(path.join(project.projectFolderPath, `${project.projectFileName}_1.publish.xml`)), + saveLabel: constants.save, + filters: { + 'Publish files': ['publish.xml'], + } + } + ); +} + +/** + * Prompt to save publish profile and add to the tree + * @param project + * @param settings Publish settings + * @returns + */ +export async function promptForSavingProfile(project: Project, settings: ISqlProjectPublishSettings | ISqlDbDeployProfile | IPublishToDockerSettings | undefined) { + const result = await vscode.window.showInformationMessage(constants.saveProfile, constants.yesString, constants.noString); + if (result === constants.yesString) { + let publishProfileUri: vscode.Uri | undefined; + if (settings) { + if (isISqlProjectPublishSettings(settings)) { + publishProfileUri = settings.publishProfileUri; + } else if (isISqlDbDeployProfile(settings)) { + publishProfileUri = settings.deploySettings?.publishProfileUri; + } else if (isIPublishToDockerSettings(settings)) { + publishProfileUri = settings.sqlProjectPublishSettings.publishProfileUri; + } + } + const filePath = await promptToSaveProfile(project, publishProfileUri); + + if (!filePath) { + return; + } + + const targetConnectionString = await getConnectionString(settings); + const targetDatabaseName = getDatabaseName(settings, project.projectFileName); + const deploymentOptions = await getDeploymentOptions(settings, project); + const sqlCmdVariables = getSqlCmdVariables(settings); + await savePublishProfile(filePath.fsPath, targetDatabaseName, targetConnectionString, sqlCmdVariables, deploymentOptions); + + setProfileParameters(settings, filePath); + + await project.addNoneItem(path.relative(project.projectFolderPath, filePath.fsPath)); + void vscode.commands.executeCommand(constants.refreshDataWorkspaceCommand); //refresh data workspace to load the newly added profile to the tree + } +} + +/** + * Function to confirm the Publish to existing server workflow + * @param settings + * @returns true if the settings is of type ISqlProjectPublishSettings + */ +function isISqlProjectPublishSettings(settings: ISqlProjectPublishSettings | ISqlDbDeployProfile | IPublishToDockerSettings | undefined): settings is ISqlProjectPublishSettings { + if ((settings as ISqlProjectPublishSettings).connectionUri) { + return true + } + return false +} + +/** + * Function to confirm the Publish to New Azure server workflow + * @param settings + * @returns true if the settings is of type ISqlDbDeployProfile + */ +function isISqlDbDeployProfile(settings: ISqlProjectPublishSettings | ISqlDbDeployProfile | IPublishToDockerSettings | undefined): settings is ISqlDbDeployProfile { + if ((settings as ISqlDbDeployProfile).deploySettings) { + return true + } + return false +} + +/** + * Function to confirm the Publish to Docker workflow + * @param settings + * @returns true if the settings is of type IPublishToDockerSettings + */ +function isIPublishToDockerSettings(settings: ISqlProjectPublishSettings | ISqlDbDeployProfile | IPublishToDockerSettings | undefined): settings is IPublishToDockerSettings { + if ((settings as IPublishToDockerSettings).dockerSettings) { + return true + } + return false +} + +async function getConnectionString(settings: ISqlProjectPublishSettings | ISqlDbDeployProfile | IPublishToDockerSettings | undefined): Promise { + let connectionUri: string = ''; + let connectionString: string = ''; + + if (settings) { + if (isISqlProjectPublishSettings(settings)) { + connectionUri = settings.connectionUri; + } else if (isISqlDbDeployProfile(settings)) { + connectionUri = settings.deploySettings?.connectionUri ?? ''; + } else if (isIPublishToDockerSettings(settings)) { + connectionUri = settings.sqlProjectPublishSettings.connectionUri; + } + } + + if (connectionUri) { + connectionString = await (await utils.getVscodeMssqlApi()).getConnectionString(connectionUri, false); + } + return connectionString; +} + +function getDatabaseName(settings: ISqlProjectPublishSettings | ISqlDbDeployProfile | IPublishToDockerSettings | undefined, projectName: string): string { + let databaseName: string = projectName; + + if (settings) { + if (isISqlProjectPublishSettings(settings)) { + databaseName = settings.databaseName; + } else if (isISqlDbDeployProfile(settings)) { + databaseName = settings.deploySettings?.databaseName ?? ''; + } else if (isIPublishToDockerSettings(settings)) { + databaseName = settings.sqlProjectPublishSettings.databaseName; + } + } + + return databaseName; +} + +async function getDeploymentOptions(settings: ISqlProjectPublishSettings | ISqlDbDeployProfile | IPublishToDockerSettings | undefined, project: Project): Promise { + let deploymentOptions: vscodeMssql.DeploymentOptions | undefined; + + if (settings) { + if (isISqlProjectPublishSettings(settings)) { + deploymentOptions = settings.deploymentOptions; + } else if (isISqlDbDeployProfile(settings)) { + deploymentOptions = settings.deploySettings?.deploymentOptions; + } else if (isIPublishToDockerSettings(settings)) { + deploymentOptions = settings.sqlProjectPublishSettings.deploymentOptions; + } + } else { + deploymentOptions = await utils.getDefaultPublishDeploymentOptions(project); + } + + return deploymentOptions; +} + +function getSqlCmdVariables(settings: ISqlProjectPublishSettings | ISqlDbDeployProfile | IPublishToDockerSettings | undefined): Map | undefined { + let sqlCmdVariables: Map | undefined; + + if (settings) { + if (isISqlProjectPublishSettings(settings)) { + sqlCmdVariables = settings.sqlCmdVariables; + } else if (isISqlDbDeployProfile(settings)) { + sqlCmdVariables = settings.deploySettings?.sqlCmdVariables; + } else if (isIPublishToDockerSettings(settings)) { + sqlCmdVariables = settings.sqlProjectPublishSettings.sqlCmdVariables; + } + } + + return sqlCmdVariables; +} + +function setProfileParameters(settings: ISqlProjectPublishSettings | ISqlDbDeployProfile | IPublishToDockerSettings | undefined, profilePath: vscode.Uri) { + if (settings) { + if (isISqlProjectPublishSettings(settings)) { + settings.publishProfileUri = profilePath; + } else if (isISqlDbDeployProfile(settings)) { + if (settings.deploySettings) { + settings.deploySettings.publishProfileUri = profilePath; + } + } else if (isIPublishToDockerSettings(settings)) { + settings.sqlProjectPublishSettings.publishProfileUri = profilePath; + } + } } diff --git a/extensions/sql-database-projects/src/test/dialogs/publishDatabaseDialog.test.ts b/extensions/sql-database-projects/src/test/dialogs/publishDatabaseDialog.test.ts index 127ab9ed36..f6d6d91b79 100644 --- a/extensions/sql-database-projects/src/test/dialogs/publishDatabaseDialog.test.ts +++ b/extensions/sql-database-projects/src/test/dialogs/publishDatabaseDialog.test.ts @@ -87,8 +87,7 @@ describe('Publish Database Dialog', () => { ['ProdDatabaseName', 'MyProdDatabase'], ['BackupDatabaseName', 'MyBackupDatabase'] ]), - deploymentOptions: mockDacFxOptionsResult.deploymentOptions, - profileUsed: false + deploymentOptions: mockDacFxOptionsResult.deploymentOptions }; dialog.object.publish = (_, prof) => { profile = prof; }; @@ -104,8 +103,7 @@ describe('Publish Database Dialog', () => { ['ProdDatabaseName', 'MyProdDatabase'], ['BackupDatabaseName', 'MyBackupDatabase'] ]), - deploymentOptions: mockDacFxOptionsResult.deploymentOptions, - profileUsed: false + deploymentOptions: mockDacFxOptionsResult.deploymentOptions }; dialog.object.generateScript = (_, prof) => { profile = prof; }; @@ -132,8 +130,7 @@ describe('Publish Database Dialog', () => { ['ProdDatabaseName', 'MyProdDatabase'], ['BackupDatabaseName', 'MyBackupDatabase'] ]), - deploymentOptions: mockDacFxOptionsResult.deploymentOptions, - profileUsed: false + deploymentOptions: mockDacFxOptionsResult.deploymentOptions } }; dialog.object.publishToExistingServer = false;