diff --git a/extensions/sql-database-projects/src/common/constants.ts b/extensions/sql-database-projects/src/common/constants.ts index 4053b19b0d..6456a90ec7 100644 --- a/extensions/sql-database-projects/src/common/constants.ts +++ b/extensions/sql-database-projects/src/common/constants.ts @@ -76,7 +76,6 @@ export const vscodeOpenCommand = 'vscode.open'; export const refreshDataWorkspaceCommand = 'dataworkspace.refresh'; // UI Strings - export const dataSourcesNodeName = localize('dataSourcesNodeName', "Data Sources"); export const databaseReferencesNodeName = localize('databaseReferencesNodeName', "Database References"); export const sqlConnectionStringFriendly = localize('sqlConnectionStringFriendly', "SQL connection string"); @@ -111,7 +110,6 @@ export function convertToSdkStyleConfirmation(projectName: string) { return loca export function updatedToSdkStyleError(projectName: string) { return localize('updatedToSdkStyleError', "Converting the project {0} to SDK-style was unsuccessful. Changes to the .sqlproj have been rolled back.", projectName); } // Publish dialog strings - export const publishDialogName = localize('publishDialogName', "Publish project"); export const publish = localize('publish', "Publish"); export const cancelButtonText = localize('cancelButtonText', "Cancel"); @@ -147,6 +145,16 @@ export const selectDatabase = localize('selectDatabase', "Select database"); export const done = localize('done', "Done"); export const nameMustNotBeEmpty = localize('nameMustNotBeEmpty', "Name must not be empty"); +// Publish Dialog options +export const publishOptions = localize('publishOptions', 'Publish Options'); +export const publishingOptions = localize('publishingOptions', 'Publishing Options'); +export const GeneralOptions: string = localize('generalOptions', "General Options"); +export const ResetButton: string = localize('reset', "Reset"); +export const OptionDescription: string = localize('optionDescription', "Option Description"); +export const OptionName: string = localize('optionName', "Option Name"); +export const OptionInclude: string = localize('Include', "Include"); + + // Deploy export const SqlServerName = 'SQL server'; export const AzureSqlServerName = 'Azure SQL server'; @@ -273,7 +281,6 @@ export const dacpacNotOnSameDrive = (projectLocation: string): string => { retur export const referenceType = localize('referenceType', "Reference type"); // Create Project From Database dialog strings - export const createProjectFromDatabaseDialogName = localize('createProjectFromDatabaseDialogName', "Create project from database"); export const createProjectDialogOkButtonText = localize('createProjectDialogOkButtonText', "Create"); export const sourceDatabase = localize('sourceDatabase', "Source database"); diff --git a/extensions/sql-database-projects/src/common/telemetry.ts b/extensions/sql-database-projects/src/common/telemetry.ts index dce8ca152f..5441ed326a 100644 --- a/extensions/sql-database-projects/src/common/telemetry.ts +++ b/extensions/sql-database-projects/src/common/telemetry.ts @@ -40,5 +40,6 @@ export enum TelemetryActions { updateProjectFromDatabase = 'updateProjectFromDatabase', publishToContainer = 'publishToContainer', publishToNewAzureServer = 'publishToNewAzureServer', - generateProjectFromOpenApiSpec = 'generateProjectFromOpenApiSpec' + generateProjectFromOpenApiSpec = 'generateProjectFromOpenApiSpec', + publishConfigureOptionsClicked = 'PublishConfigureOptionsClicked' } diff --git a/extensions/sql-database-projects/src/common/uiConstants.ts b/extensions/sql-database-projects/src/common/uiConstants.ts index f87e78942f..d42f87427d 100644 --- a/extensions/sql-database-projects/src/common/uiConstants.ts +++ b/extensions/sql-database-projects/src/common/uiConstants.ts @@ -13,6 +13,8 @@ export namespace cssStyles { export const publishDialogLabelWidth = '205px'; export const publishDialogTextboxWidth = '190px'; + export const publishDialogDropdownWidth = '192px'; + export const PublishingOptionsButtonWidth = '120px'; export const addDatabaseReferenceDialogLabelWidth = '215px'; export const addDatabaseReferenceInputboxWidth = '220px'; diff --git a/extensions/sql-database-projects/src/dialogs/publishDatabaseDialog.ts b/extensions/sql-database-projects/src/dialogs/publishDatabaseDialog.ts index d479bc4031..2f35db041b 100644 --- a/extensions/sql-database-projects/src/dialogs/publishDatabaseDialog.ts +++ b/extensions/sql-database-projects/src/dialogs/publishDatabaseDialog.ts @@ -18,6 +18,7 @@ import { getAgreementDisplayText, getConnectionName, getDockerBaseImages, getPub import { TelemetryActions, TelemetryReporter, TelemetryViews } from '../common/telemetry'; import { ILocalDbDeployProfile } from '../models/deploy/deployProfile'; import { Deferred } from '../common/promise'; +import { PublishOptionsDialog } from './publishOptionsDialog'; interface DataSourceDropdownValue extends azdataType.CategoryValue { dataSource: SqlConnectionDataSource; @@ -56,6 +57,8 @@ export class PublishDatabaseDialog { private deploymentOptions: DeploymentOptions | undefined; private profileUsed: boolean = false; private serverName: string | undefined; + protected optionsButton: azdataType.ButtonComponent | undefined; + private publishOptionsDialog: PublishOptionsDialog | undefined; private completionPromise: Deferred = new Deferred(); @@ -134,13 +137,17 @@ export class PublishDatabaseDialog { title: constants.sqlCmdVariables }; + // Get the default deployment option and set + const options = await this.getDefaultDeploymentOptions(); + this.setDeploymentOptions(options); + const profileRow = this.createProfileRow(view); this.connectionRow = this.createConnectionRow(view); this.databaseRow = this.createDatabaseRow(view); + const displayOptionsButton = this.createOptionsButton(view); const horizontalFormSection = view.modelBuilder.flexContainer().withLayout({ flexFlow: 'column' }).component(); - horizontalFormSection.addItems([profileRow, this.databaseRow]); - + horizontalFormSection.addItems([profileRow, this.databaseRow, displayOptionsButton]); this.formBuilder = view.modelBuilder.formContainer() .withFormItems([ @@ -281,8 +288,6 @@ export class PublishDatabaseDialog { } public async getDeploymentOptions(): Promise { - // eventually, database options will be configurable in this dialog - // but for now, just send the default DacFx deployment options if no options were loaded from a publish profile if (!this.deploymentOptions) { // We only use the dialog in ADS context currently so safe to cast to the mssql DeploymentOptions here this.deploymentOptions = await utils.getDefaultPublishDeploymentOptions(this.project) as DeploymentOptions; @@ -534,7 +539,7 @@ export class PublishDatabaseDialog { width: cssStyles.publishDialogLabelWidth }).component(); - const connectionRow = view.modelBuilder.flexContainer().withItems([serverLabel, this.targetConnectionTextBox], { flex: '0 0 auto', CSSStyles: { 'margin-right': '10px' } }).withLayout({ flexFlow: 'row', alignItems: 'center' }).component(); + const connectionRow = view.modelBuilder.flexContainer().withItems([serverLabel, this.targetConnectionTextBox], { flex: '0 0 auto', CSSStyles: { 'margin': '-8px 10px -15px 0' } }).withLayout({ flexFlow: 'row', alignItems: 'center' }).component(); connectionRow.insertItem(selectConnectionButton, 2, { CSSStyles: { 'margin-right': '0px' } }); return connectionRow; @@ -650,7 +655,7 @@ export class PublishDatabaseDialog { this.targetDatabaseTextBox = view.modelBuilder.inputBox().withProps({ ariaLabel: constants.databaseNameLabel, required: true, - width: cssStyles.publishDialogTextboxWidth, + width: cssStyles.publishDialogDropdownWidth, value: this.getDefaultDatabaseName() }).component(); } @@ -662,7 +667,7 @@ export class PublishDatabaseDialog { value: this.getDefaultDatabaseName(), ariaLabel: constants.databaseNameLabel, required: true, - width: cssStyles.publishDialogTextboxWidth, + width: cssStyles.publishDialogDropdownWidth, editable: true, fireOnTextChange: true }).component(); @@ -896,6 +901,42 @@ export class PublishDatabaseDialog { return true; } + + /* + * Creates Display options container with a 'configure options' button + */ + private createOptionsButton(view: azdataType.ModelView): azdataType.FlexContainer { + this.optionsButton = view.modelBuilder.button().withProps({ + label: constants.publishingOptions, + secondary: true, + width: cssStyles.PublishingOptionsButtonWidth + }).component(); + + const optionsRow = view.modelBuilder.flexContainer().withItems([this.optionsButton], { CSSStyles: { flex: '0 0 auto', 'margin': '6px 0 0 287px' } }).withLayout({ flexFlow: 'row', alignItems: 'center' }).component(); + + this.toDispose.push(this.optionsButton.onDidClick(async () => { + TelemetryReporter.sendActionEvent(TelemetryViews.SqlProjectPublishDialog, TelemetryActions.publishConfigureOptionsClicked); + // Create fresh options dialog with default selections each time when creating the 'configure options' button + this.publishOptionsDialog = new PublishOptionsDialog(this.deploymentOptions!, this); + this.publishOptionsDialog.openDialog(); + })); + + return optionsRow; + } + + /* + * Gets the default deployment options from the dacfx service + */ + public async getDefaultDeploymentOptions(): Promise { + return await utils.getDefaultPublishDeploymentOptions(this.project) as DeploymentOptions; + } + + /* + * Sets the default deployment options to deployment options model object + */ + public setDeploymentOptions(deploymentOptions: DeploymentOptions): void { + this.deploymentOptions = deploymentOptions; + } } export function promptForPublishProfile(defaultPath: string): Thenable { diff --git a/extensions/sql-database-projects/src/dialogs/publishOptionsDialog.ts b/extensions/sql-database-projects/src/dialogs/publishOptionsDialog.ts new file mode 100644 index 0000000000..0d1534aafc --- /dev/null +++ b/extensions/sql-database-projects/src/dialogs/publishOptionsDialog.ts @@ -0,0 +1,167 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as constants from '../common/constants'; +import * as vscode from 'vscode'; +import * as mssql from 'mssql'; +import * as utils from '../common/utils'; +import type * as azdataType from 'azdata'; +import { PublishDatabaseDialog } from './publishDatabaseDialog'; +import { DeployOptionsModel } from '../models/options/deployOptionsModel'; + +export class PublishOptionsDialog { + + public dialog!: azdataType.window.Dialog; + private optionsTab: azdataType.window.DialogTab | undefined; + private disposableListeners: vscode.Disposable[] = []; + private descriptionHeading: azdataType.TableComponent | undefined; + private descriptionText: azdataType.TextComponent | undefined; + private optionsTable: azdataType.TableComponent | undefined; + public optionsModel: DeployOptionsModel; + private optionsFlexBuilder: azdataType.FlexContainer | undefined; + + constructor(defaultOptions: mssql.DeploymentOptions, private publish: PublishDatabaseDialog) { + this.optionsModel = new DeployOptionsModel(defaultOptions); + } + + protected initializeDialog(): void { + this.optionsTab = utils.getAzdataApi()!.window.createTab(constants.publishOptions); + this.intializeDeploymentOptionsDialogTab(); + this.dialog.content = [this.optionsTab]; + } + + public openDialog(): void { + this.dialog = utils.getAzdataApi()!.window.createModelViewDialog(constants.publishOptions); + + this.initializeDialog(); + + this.dialog.okButton.label = constants.okString; + this.dialog.okButton.onClick(() => this.execute()); + + this.dialog.cancelButton.label = constants.cancelButtonText; + this.dialog.cancelButton.onClick(() => this.cancel()); + + let resetButton = utils.getAzdataApi()!.window.createButton(constants.ResetButton); + resetButton.onClick(async () => await this.reset()); + this.dialog.customButtons = [resetButton]; + + utils.getAzdataApi()!.window.openDialog(this.dialog); + } + + private intializeDeploymentOptionsDialogTab(): void { + this.optionsTab?.registerContent(async view => { + this.descriptionHeading = view.modelBuilder.table().withProps({ + data: [], + columns: [ + { + value: constants.OptionDescription, + headerCssClass: 'no-borders', + toolTip: constants.OptionDescription + } + ] + }).component(); + + this.descriptionText = view.modelBuilder.text().withProps({ + value: ' ' + }).component(); + + this.optionsTable = view.modelBuilder.table().component(); + await this.updateOptionsTable(); + + // Get the description of the selected option + this.disposableListeners.push(this.optionsTable.onRowSelected(async () => { + const row = this.optionsTable?.selectedRows![0]; + const label = this.optionsModel.optionsLabels[row!]; + await this.descriptionText?.updateProperties({ + value: this.optionsModel.getDescription(label) + }); + })); + + // Update deploy options value on checkbox onchange + this.disposableListeners.push(this.optionsTable.onCellAction!((rowState) => { + const checkboxState = rowState; + if (checkboxState && checkboxState.row !== undefined) { + const label = this.optionsModel.optionsLabels[checkboxState.row]; + this.optionsModel.optionsLookup[label] = checkboxState.checked; + } + })); + + this.optionsFlexBuilder = view.modelBuilder.flexContainer() + .withLayout({ + flexFlow: 'column' + }).component(); + + this.optionsFlexBuilder.addItem(this.optionsTable, { CSSStyles: { 'overflow': 'scroll', 'height': '65vh' } }); + this.optionsFlexBuilder.addItem(this.descriptionHeading, { CSSStyles: { 'font-weight': 'bold', 'height': '30px' } }); + this.optionsFlexBuilder.addItem(this.descriptionText, { CSSStyles: { 'padding': '4px', 'margin-right': '10px', 'overflow': 'scroll', 'height': '10vh' } }); + await view.initializeModel(this.optionsFlexBuilder); + // focus the first option + await this.optionsTable.focus(); + }); + } + + /* + * Update the default options to the options table area + */ + private async updateOptionsTable(): Promise { + const data = this.optionsModel.getOptionsData(); + await this.optionsTable?.updateProperties({ + data: data, + columns: [ + + { + value: constants.OptionInclude, + type: utils.getAzdataApi()!.ColumnType.checkBox, + action: utils.getAzdataApi()!.ActionOnCellCheckboxCheck.customAction, + headerCssClass: 'display-none', + cssClass: 'no-borders align-with-header', + width: 50 + }, + { + value: constants.OptionName, + headerCssClass: 'display-none', + cssClass: 'no-borders align-with-header', + width: 50 + } + ], + ariaRowCount: data.length + }); + } + + /* + * Ok button click, will update the deployment options with selections + */ + protected execute(): void { + this.optionsModel.setDeploymentOptions(); + this.publish.setDeploymentOptions(this.optionsModel.deploymentOptions); + this.disposeListeners(); + } + + /* + * Cancels the deploy options table dialog and its changes will be disposed + */ + protected cancel(): void { + this.disposeListeners(); + } + + /* + * Reset button click, resets all the options selection + */ + private async reset(): Promise { + const result = await this.publish.getDefaultDeploymentOptions(); + this.optionsModel.deploymentOptions = result; + + // This will update the Map table with default values + this.optionsModel.InitializeUpdateOptionsMapTable(); + + await this.updateOptionsTable(); + this.optionsFlexBuilder?.removeItem(this.optionsTable!); + this.optionsFlexBuilder?.insertItem(this.optionsTable!, 0, { CSSStyles: { 'overflow': 'scroll', 'height': '65vh' } }); + } + + private disposeListeners(): void { + this.disposableListeners.forEach(x => x.dispose()); + } +} diff --git a/extensions/sql-database-projects/src/models/options/deployOptionsModel.ts b/extensions/sql-database-projects/src/models/options/deployOptionsModel.ts new file mode 100644 index 0000000000..4f18419e2a --- /dev/null +++ b/extensions/sql-database-projects/src/models/options/deployOptionsModel.ts @@ -0,0 +1,257 @@ +/*--------------------------------------------------------------------------------------------- + * 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'; + +export class DeployOptionsModel { + public deploymentOptions: mssql.DeploymentOptions; + + public optionsLookup: Record = {}; + public optionsMapTable: Record = {}; + + constructor(defaultOptions: mssql.DeploymentOptions) { + this.deploymentOptions = defaultOptions; + this.InitializeUpdateOptionsMapTable(); + this.InitializeOptionsLabels(); + } + + /* + * Initialize the options mapping table + * This will map the key:Option_DisplayName to the value:DacFx_OptionsValue + */ + public InitializeUpdateOptionsMapTable() { + this.optionsMapTable[this.deploymentOptions.ignoreTableOptions.displayName] = this.deploymentOptions.ignoreTableOptions; + this.optionsMapTable[this.deploymentOptions.ignoreSemicolonBetweenStatements.displayName] = this.deploymentOptions.ignoreSemicolonBetweenStatements; + this.optionsMapTable[this.deploymentOptions.ignoreRouteLifetime.displayName] = this.deploymentOptions.ignoreRouteLifetime; + this.optionsMapTable[this.deploymentOptions.ignoreRoleMembership.displayName] = this.deploymentOptions.ignoreRoleMembership; + this.optionsMapTable[this.deploymentOptions.ignoreQuotedIdentifiers.displayName] = this.deploymentOptions.ignoreQuotedIdentifiers; + this.optionsMapTable[this.deploymentOptions.ignorePermissions.displayName] = this.deploymentOptions.ignorePermissions; + this.optionsMapTable[this.deploymentOptions.ignorePartitionSchemes.displayName] = this.deploymentOptions.ignorePartitionSchemes; + this.optionsMapTable[this.deploymentOptions.ignoreObjectPlacementOnPartitionScheme.displayName] = this.deploymentOptions.ignoreObjectPlacementOnPartitionScheme; + this.optionsMapTable[this.deploymentOptions.ignoreNotForReplication.displayName] = this.deploymentOptions.ignoreNotForReplication; + this.optionsMapTable[this.deploymentOptions.ignoreLoginSids.displayName] = this.deploymentOptions.ignoreLoginSids; + this.optionsMapTable[this.deploymentOptions.ignoreLockHintsOnIndexes.displayName] = this.deploymentOptions.ignoreLockHintsOnIndexes; + this.optionsMapTable[this.deploymentOptions.ignoreKeywordCasing.displayName] = this.deploymentOptions.ignoreKeywordCasing; + this.optionsMapTable[this.deploymentOptions.ignoreIndexPadding.displayName] = this.deploymentOptions.ignoreIndexPadding; + this.optionsMapTable[this.deploymentOptions.ignoreIndexOptions.displayName] = this.deploymentOptions.ignoreIndexOptions; + this.optionsMapTable[this.deploymentOptions.ignoreIncrement.displayName] = this.deploymentOptions.ignoreIncrement; + this.optionsMapTable[this.deploymentOptions.ignoreIdentitySeed.displayName] = this.deploymentOptions.ignoreIdentitySeed; + this.optionsMapTable[this.deploymentOptions.ignoreUserSettingsObjects.displayName] = this.deploymentOptions.ignoreUserSettingsObjects; + this.optionsMapTable[this.deploymentOptions.ignoreFullTextCatalogFilePath.displayName] = this.deploymentOptions.ignoreFullTextCatalogFilePath; + this.optionsMapTable[this.deploymentOptions.ignoreWhitespace.displayName] = this.deploymentOptions.ignoreWhitespace; + this.optionsMapTable[this.deploymentOptions.ignoreWithNocheckOnForeignKeys.displayName] = this.deploymentOptions.ignoreWithNocheckOnForeignKeys; + this.optionsMapTable[this.deploymentOptions.verifyCollationCompatibility.displayName] = this.deploymentOptions.verifyCollationCompatibility; + this.optionsMapTable[this.deploymentOptions.unmodifiableObjectWarnings.displayName] = this.deploymentOptions.unmodifiableObjectWarnings; + this.optionsMapTable[this.deploymentOptions.treatVerificationErrorsAsWarnings.displayName] = this.deploymentOptions.treatVerificationErrorsAsWarnings; + this.optionsMapTable[this.deploymentOptions.scriptRefreshModule.displayName] = this.deploymentOptions.scriptRefreshModule; + this.optionsMapTable[this.deploymentOptions.scriptNewConstraintValidation.displayName] = this.deploymentOptions.scriptNewConstraintValidation; + this.optionsMapTable[this.deploymentOptions.scriptFileSize.displayName] = this.deploymentOptions.scriptFileSize; + this.optionsMapTable[this.deploymentOptions.scriptDeployStateChecks.displayName] = this.deploymentOptions.scriptDeployStateChecks; + this.optionsMapTable[this.deploymentOptions.scriptDatabaseOptions.displayName] = this.deploymentOptions.scriptDatabaseOptions; + this.optionsMapTable[this.deploymentOptions.scriptDatabaseCompatibility.displayName] = this.deploymentOptions.scriptDatabaseCompatibility; + this.optionsMapTable[this.deploymentOptions.scriptDatabaseCollation.displayName] = this.deploymentOptions.scriptDatabaseCollation; + this.optionsMapTable[this.deploymentOptions.runDeploymentPlanExecutors.displayName] = this.deploymentOptions.runDeploymentPlanExecutors; + this.optionsMapTable[this.deploymentOptions.registerDataTierApplication.displayName] = this.deploymentOptions.registerDataTierApplication; + this.optionsMapTable[this.deploymentOptions.populateFilesOnFileGroups.displayName] = this.deploymentOptions.populateFilesOnFileGroups; + this.optionsMapTable[this.deploymentOptions.noAlterStatementsToChangeClrTypes.displayName] = this.deploymentOptions.noAlterStatementsToChangeClrTypes; + this.optionsMapTable[this.deploymentOptions.includeTransactionalScripts.displayName] = this.deploymentOptions.includeTransactionalScripts; + this.optionsMapTable[this.deploymentOptions.includeCompositeObjects.displayName] = this.deploymentOptions.includeCompositeObjects; + this.optionsMapTable[this.deploymentOptions.allowUnsafeRowLevelSecurityDataMovement.displayName] = this.deploymentOptions.allowUnsafeRowLevelSecurityDataMovement; + this.optionsMapTable[this.deploymentOptions.ignoreWithNocheckOnCheckConstraints.displayName] = this.deploymentOptions.ignoreWithNocheckOnCheckConstraints; + this.optionsMapTable[this.deploymentOptions.ignoreFillFactor.displayName] = this.deploymentOptions.ignoreFillFactor; + this.optionsMapTable[this.deploymentOptions.ignoreFileSize.displayName] = this.deploymentOptions.ignoreFileSize; + this.optionsMapTable[this.deploymentOptions.ignoreFilegroupPlacement.displayName] = this.deploymentOptions.ignoreFilegroupPlacement; + this.optionsMapTable[this.deploymentOptions.doNotAlterReplicatedObjects.displayName] = this.deploymentOptions.doNotAlterReplicatedObjects; + this.optionsMapTable[this.deploymentOptions.doNotAlterChangeDataCaptureObjects.displayName] = this.deploymentOptions.doNotAlterChangeDataCaptureObjects; + this.optionsMapTable[this.deploymentOptions.disableAndReenableDdlTriggers.displayName] = this.deploymentOptions.disableAndReenableDdlTriggers; + this.optionsMapTable[this.deploymentOptions.deployDatabaseInSingleUserMode.displayName] = this.deploymentOptions.deployDatabaseInSingleUserMode; + this.optionsMapTable[this.deploymentOptions.createNewDatabase.displayName] = this.deploymentOptions.createNewDatabase; + this.optionsMapTable[this.deploymentOptions.compareUsingTargetCollation.displayName] = this.deploymentOptions.compareUsingTargetCollation; + this.optionsMapTable[this.deploymentOptions.commentOutSetVarDeclarations.displayName] = this.deploymentOptions.commentOutSetVarDeclarations; + this.optionsMapTable[this.deploymentOptions.blockWhenDriftDetected.displayName] = this.deploymentOptions.blockWhenDriftDetected; + this.optionsMapTable[this.deploymentOptions.blockOnPossibleDataLoss.displayName] = this.deploymentOptions.blockOnPossibleDataLoss; + this.optionsMapTable[this.deploymentOptions.backupDatabaseBeforeChanges.displayName] = this.deploymentOptions.backupDatabaseBeforeChanges; + this.optionsMapTable[this.deploymentOptions.allowIncompatiblePlatform.displayName] = this.deploymentOptions.allowIncompatiblePlatform; + this.optionsMapTable[this.deploymentOptions.allowDropBlockingAssemblies.displayName] = this.deploymentOptions.allowDropBlockingAssemblies; + this.optionsMapTable[this.deploymentOptions.dropConstraintsNotInSource.displayName] = this.deploymentOptions.dropConstraintsNotInSource; + this.optionsMapTable[this.deploymentOptions.dropDmlTriggersNotInSource.displayName] = this.deploymentOptions.dropDmlTriggersNotInSource; + this.optionsMapTable[this.deploymentOptions.dropExtendedPropertiesNotInSource.displayName] = this.deploymentOptions.dropExtendedPropertiesNotInSource; + this.optionsMapTable[this.deploymentOptions.dropIndexesNotInSource.displayName] = this.deploymentOptions.dropIndexesNotInSource; + this.optionsMapTable[this.deploymentOptions.ignoreFileAndLogFilePath.displayName] = this.deploymentOptions.ignoreFileAndLogFilePath; + this.optionsMapTable[this.deploymentOptions.ignoreExtendedProperties.displayName] = this.deploymentOptions.ignoreExtendedProperties; + this.optionsMapTable[this.deploymentOptions.ignoreDmlTriggerState.displayName] = this.deploymentOptions.ignoreDmlTriggerState; + this.optionsMapTable[this.deploymentOptions.ignoreDmlTriggerOrder.displayName] = this.deploymentOptions.ignoreDmlTriggerOrder; + this.optionsMapTable[this.deploymentOptions.ignoreDefaultSchema.displayName] = this.deploymentOptions.ignoreDefaultSchema; + this.optionsMapTable[this.deploymentOptions.ignoreDdlTriggerState.displayName] = this.deploymentOptions.ignoreDdlTriggerState; + this.optionsMapTable[this.deploymentOptions.ignoreDdlTriggerOrder.displayName] = this.deploymentOptions.ignoreDdlTriggerOrder; + this.optionsMapTable[this.deploymentOptions.ignoreCryptographicProviderFilePath.displayName] = this.deploymentOptions.ignoreCryptographicProviderFilePath; + this.optionsMapTable[this.deploymentOptions.verifyDeployment.displayName] = this.deploymentOptions.verifyDeployment; + this.optionsMapTable[this.deploymentOptions.ignoreComments.displayName] = this.deploymentOptions.ignoreComments; + this.optionsMapTable[this.deploymentOptions.ignoreColumnCollation.displayName] = this.deploymentOptions.ignoreColumnCollation; + this.optionsMapTable[this.deploymentOptions.ignoreAuthorizer.displayName] = this.deploymentOptions.ignoreAuthorizer; + this.optionsMapTable[this.deploymentOptions.ignoreAnsiNulls.displayName] = this.deploymentOptions.ignoreAnsiNulls; + this.optionsMapTable[this.deploymentOptions.generateSmartDefaults.displayName] = this.deploymentOptions.generateSmartDefaults; + this.optionsMapTable[this.deploymentOptions.dropStatisticsNotInSource.displayName] = this.deploymentOptions.dropStatisticsNotInSource; + this.optionsMapTable[this.deploymentOptions.dropRoleMembersNotInSource.displayName] = this.deploymentOptions.dropRoleMembersNotInSource; + this.optionsMapTable[this.deploymentOptions.dropPermissionsNotInSource.displayName] = this.deploymentOptions.dropPermissionsNotInSource; + this.optionsMapTable[this.deploymentOptions.dropObjectsNotInSource.displayName] = this.deploymentOptions.dropObjectsNotInSource; + this.optionsMapTable[this.deploymentOptions.ignoreColumnOrder.displayName] = this.deploymentOptions.ignoreColumnOrder; + this.optionsMapTable[this.deploymentOptions.ignoreTablePartitionOptions.displayName] = this.deploymentOptions.ignoreTablePartitionOptions; + this.optionsMapTable[this.deploymentOptions.doNotEvaluateSqlCmdVariables.displayName] = this.deploymentOptions.doNotEvaluateSqlCmdVariables; + this.optionsMapTable[this.deploymentOptions.disableParallelismForEnablingIndexes.displayName] = this.deploymentOptions.disableParallelismForEnablingIndexes; + this.optionsMapTable[this.deploymentOptions.disableIndexesForDataPhase.displayName] = this.deploymentOptions.disableIndexesForDataPhase; + this.optionsMapTable[this.deploymentOptions.restoreSequenceCurrentValue.displayName] = this.deploymentOptions.restoreSequenceCurrentValue; + this.optionsMapTable[this.deploymentOptions.rebuildIndexesOfflineForDataPhase.displayName] = this.deploymentOptions.rebuildIndexesOfflineForDataPhase; + this.optionsMapTable[this.deploymentOptions.preserveIdentityLastValues.displayName] = this.deploymentOptions.preserveIdentityLastValues; + this.optionsMapTable[this.deploymentOptions.isAlwaysEncryptedParameterizationEnabled.displayName] = this.deploymentOptions.isAlwaysEncryptedParameterizationEnabled; + this.optionsMapTable[this.deploymentOptions.allowExternalLibraryPaths.displayName] = this.deploymentOptions.allowExternalLibraryPaths; + this.optionsMapTable[this.deploymentOptions.allowExternalLanguagePaths.displayName] = this.deploymentOptions.allowExternalLanguagePaths; + this.optionsMapTable[this.deploymentOptions.hashObjectNamesInLogs.displayName] = this.deploymentOptions.hashObjectNamesInLogs; + this.optionsMapTable[this.deploymentOptions.doNotDropWorkloadClassifiers.displayName] = this.deploymentOptions.doNotDropWorkloadClassifiers; + this.optionsMapTable[this.deploymentOptions.ignoreWorkloadClassifiers.displayName] = this.deploymentOptions.ignoreWorkloadClassifiers; + this.optionsMapTable[this.deploymentOptions.ignoreDatabaseWorkloadGroups.displayName] = this.deploymentOptions.ignoreDatabaseWorkloadGroups; + this.optionsMapTable[this.deploymentOptions.doNotDropDatabaseWorkloadGroups.displayName] = this.deploymentOptions.doNotDropDatabaseWorkloadGroups; + } + + /* + * List of Dac Deploy options to display + */ + public optionsLabels: string[] = []; + public InitializeOptionsLabels() { + this.optionsLabels = [this.deploymentOptions.ignoreTableOptions.displayName + , this.deploymentOptions.ignoreSemicolonBetweenStatements.displayName + , this.deploymentOptions.ignoreRouteLifetime.displayName + , this.deploymentOptions.ignoreRoleMembership.displayName + , this.deploymentOptions.ignoreQuotedIdentifiers.displayName + , this.deploymentOptions.ignorePermissions.displayName + , this.deploymentOptions.ignorePartitionSchemes.displayName + , this.deploymentOptions.ignoreObjectPlacementOnPartitionScheme.displayName + , this.deploymentOptions.ignoreNotForReplication.displayName + , this.deploymentOptions.ignoreLoginSids.displayName + , this.deploymentOptions.ignoreLockHintsOnIndexes.displayName + , this.deploymentOptions.ignoreKeywordCasing.displayName + , this.deploymentOptions.ignoreIndexPadding.displayName + , this.deploymentOptions.ignoreIndexOptions.displayName + , this.deploymentOptions.ignoreIncrement.displayName + , this.deploymentOptions.ignoreIdentitySeed.displayName + , this.deploymentOptions.ignoreUserSettingsObjects.displayName + , this.deploymentOptions.ignoreFullTextCatalogFilePath.displayName + , this.deploymentOptions.ignoreWhitespace.displayName + , this.deploymentOptions.ignoreWithNocheckOnForeignKeys.displayName + , this.deploymentOptions.verifyCollationCompatibility.displayName + , this.deploymentOptions.unmodifiableObjectWarnings.displayName + , this.deploymentOptions.treatVerificationErrorsAsWarnings.displayName + , this.deploymentOptions.scriptRefreshModule.displayName + , this.deploymentOptions.scriptNewConstraintValidation.displayName + , this.deploymentOptions.scriptFileSize.displayName + , this.deploymentOptions.scriptDeployStateChecks.displayName + , this.deploymentOptions.scriptDatabaseOptions.displayName + , this.deploymentOptions.scriptDatabaseCompatibility.displayName + , this.deploymentOptions.scriptDatabaseCollation.displayName + , this.deploymentOptions.runDeploymentPlanExecutors.displayName + , this.deploymentOptions.registerDataTierApplication.displayName + , this.deploymentOptions.populateFilesOnFileGroups.displayName + , this.deploymentOptions.noAlterStatementsToChangeClrTypes.displayName + , this.deploymentOptions.includeTransactionalScripts.displayName + , this.deploymentOptions.includeCompositeObjects.displayName + , this.deploymentOptions.allowUnsafeRowLevelSecurityDataMovement.displayName + , this.deploymentOptions.ignoreWithNocheckOnCheckConstraints.displayName + , this.deploymentOptions.ignoreFillFactor.displayName + , this.deploymentOptions.ignoreFileSize.displayName + , this.deploymentOptions.ignoreFilegroupPlacement.displayName + , this.deploymentOptions.doNotAlterReplicatedObjects.displayName + , this.deploymentOptions.doNotAlterChangeDataCaptureObjects.displayName + , this.deploymentOptions.disableAndReenableDdlTriggers.displayName + , this.deploymentOptions.deployDatabaseInSingleUserMode.displayName + , this.deploymentOptions.createNewDatabase.displayName + , this.deploymentOptions.compareUsingTargetCollation.displayName + , this.deploymentOptions.commentOutSetVarDeclarations.displayName + , this.deploymentOptions.blockWhenDriftDetected.displayName + , this.deploymentOptions.blockOnPossibleDataLoss.displayName + , this.deploymentOptions.backupDatabaseBeforeChanges.displayName + , this.deploymentOptions.allowIncompatiblePlatform.displayName + , this.deploymentOptions.allowDropBlockingAssemblies.displayName + , this.deploymentOptions.dropConstraintsNotInSource.displayName + , this.deploymentOptions.dropDmlTriggersNotInSource.displayName + , this.deploymentOptions.dropExtendedPropertiesNotInSource.displayName + , this.deploymentOptions.dropIndexesNotInSource.displayName + , this.deploymentOptions.ignoreFileAndLogFilePath.displayName + , this.deploymentOptions.ignoreExtendedProperties.displayName + , this.deploymentOptions.ignoreDmlTriggerState.displayName + , this.deploymentOptions.ignoreDmlTriggerOrder.displayName + , this.deploymentOptions.ignoreDefaultSchema.displayName + , this.deploymentOptions.ignoreDdlTriggerState.displayName + , this.deploymentOptions.ignoreDdlTriggerOrder.displayName + , this.deploymentOptions.ignoreCryptographicProviderFilePath.displayName + , this.deploymentOptions.verifyDeployment.displayName + , this.deploymentOptions.ignoreComments.displayName + , this.deploymentOptions.ignoreColumnCollation.displayName + , this.deploymentOptions.ignoreAuthorizer.displayName + , this.deploymentOptions.ignoreAnsiNulls.displayName + , this.deploymentOptions.generateSmartDefaults.displayName + , this.deploymentOptions.dropStatisticsNotInSource.displayName + , this.deploymentOptions.dropRoleMembersNotInSource.displayName + , this.deploymentOptions.dropPermissionsNotInSource.displayName + , this.deploymentOptions.dropObjectsNotInSource.displayName + , this.deploymentOptions.ignoreColumnOrder.displayName + , this.deploymentOptions.ignoreTablePartitionOptions.displayName + , this.deploymentOptions.doNotEvaluateSqlCmdVariables.displayName + , this.deploymentOptions.disableParallelismForEnablingIndexes.displayName + , this.deploymentOptions.disableIndexesForDataPhase.displayName + , this.deploymentOptions.restoreSequenceCurrentValue.displayName + , this.deploymentOptions.rebuildIndexesOfflineForDataPhase.displayName + , this.deploymentOptions.preserveIdentityLastValues.displayName + , this.deploymentOptions.isAlwaysEncryptedParameterizationEnabled.displayName + , this.deploymentOptions.allowExternalLibraryPaths.displayName + , this.deploymentOptions.allowExternalLanguagePaths.displayName + , this.deploymentOptions.hashObjectNamesInLogs.displayName + , this.deploymentOptions.doNotDropWorkloadClassifiers.displayName + , this.deploymentOptions.ignoreWorkloadClassifiers.displayName + , this.deploymentOptions.ignoreDatabaseWorkloadGroups.displayName + , this.deploymentOptions.doNotDropDatabaseWorkloadGroups.displayName].sort(); + } + + /** + * Gets the options checkbox check value + * @returns string[][] + */ + public getOptionsData(): string[][] { + let data: any = []; + this.optionsLookup = {}; + this.optionsLabels.forEach(l => { + let checked: boolean = this.getDeployOptionUtil(l); + data.push([checked, l]); + this.optionsLookup[l] = checked; + }); + return data; + } + + public setDeploymentOptions() { + for (let option in this.optionsLookup) { + this.setDeployOptionUtil(option, this.optionsLookup[option]); + } + } + + /* + * Sets the selected/changed value of the option + */ + public setDeployOptionUtil(label: string, value: boolean) { + this.optionsMapTable[label].value = value; + } + + /* + * Gets the selected/default value of the option + */ + public getDeployOptionUtil(label: string): boolean { + return this.optionsMapTable[label]?.value; + } + + /* + * Gets the description of the option selected + */ + public getDescription(label: string): string { + return this.optionsMapTable[label]?.description; + } +} diff --git a/extensions/sql-database-projects/src/test/dialogs/publishOptionsDialog.test.ts b/extensions/sql-database-projects/src/test/dialogs/publishOptionsDialog.test.ts new file mode 100644 index 0000000000..683c6d00ec --- /dev/null +++ b/extensions/sql-database-projects/src/test/dialogs/publishOptionsDialog.test.ts @@ -0,0 +1,52 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as should from 'should'; +import * as testUtils from '../testUtils'; +import * as testData from '../testContext'; +import * as baselines from '../baselines/baselines'; +import * as TypeMoq from 'typemoq'; + +import { PublishOptionsDialog } from '../../dialogs/publishOptionsDialog'; +import { PublishDatabaseDialog } from '../../dialogs/publishDatabaseDialog'; +import { Project } from '../../models/project'; + +describe('Publish Database Options Dialog', () => { + before(async function (): Promise { + await baselines.loadBaselines(); + }); + + it('Should open dialog successfully ', async function (): Promise { + const publishDatabaseDialog = new PublishDatabaseDialog(new Project('')); + const optionsDialog = new PublishOptionsDialog(testData.getDeploymentOptions(), publishDatabaseDialog); + optionsDialog.openDialog(); + + // Verify the dialog should exists + should.notEqual(optionsDialog.dialog, undefined); + }); + + it('Should deployment options gets initialized correctly with sample test project', async function (): Promise { + // Create new sample test project + const project = await testUtils.createTestProject(baselines.openProjectFileBaseline); + const dialog = TypeMoq.Mock.ofType(PublishDatabaseDialog, undefined, undefined, project); + + dialog.setup(x => x.getDeploymentOptions()).returns(() => { return Promise.resolve(testData.mockDacFxOptionsResult.deploymentOptions); }); + + const options = await dialog.object.getDeploymentOptions(); + const optionsDialog = new PublishOptionsDialog(options, new PublishDatabaseDialog(project)); + + // Verify the options model should exists + should.notEqual(optionsDialog.optionsModel, undefined); + + // Verify the deployment options should exists + should.notEqual(optionsDialog.optionsModel.deploymentOptions, undefined); + + Object.entries(optionsDialog.optionsModel.deploymentOptions).forEach(option => { + // Validate the value and description as expected + should.equal(option[1].value, false); + should.equal(option[1].description, 'Sample Description text'); + }); + }); +}); diff --git a/extensions/sql-database-projects/src/test/models/deployOptionsModel.test.ts b/extensions/sql-database-projects/src/test/models/deployOptionsModel.test.ts new file mode 100644 index 0000000000..5acaae9697 --- /dev/null +++ b/extensions/sql-database-projects/src/test/models/deployOptionsModel.test.ts @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as should from 'should'; +import * as testUtils from '../../test/testContext'; +import { DeployOptionsModel } from '../../models/options/deployOptionsModel'; + +describe('Publish Dialog Deploy Options Model', () => { + it('Should create model and set options successfully', function (): void { + const model = new DeployOptionsModel(testUtils.getDeploymentOptions()); + should.notEqual(model.getOptionsData(), undefined, 'Options shouldn\'t be undefined'); + + should.doesNotThrow(() => model.setDeploymentOptions()); + + should(model.getDeployOptionUtil('')).equal(undefined); + }); + + it('Should get description', function (): void { + const model = new DeployOptionsModel(testUtils.getDeploymentOptions()); + model.optionsLabels.forEach(l => { + should(model.getDescription(l)).not.equal(undefined); + }); + }); + + it('Should be undefined for null description', function (): void { + const model = new DeployOptionsModel(testUtils.getDeploymentOptions()); + should(model.getDescription('')).equal(undefined); + }); +});