diff --git a/extensions/sql-database-projects/src/common/constants.ts b/extensions/sql-database-projects/src/common/constants.ts index cfa55dfaee..8be1273be3 100644 --- a/extensions/sql-database-projects/src/common/constants.ts +++ b/extensions/sql-database-projects/src/common/constants.ts @@ -133,6 +133,11 @@ export const selectPublishOption = localize('selectPublishOption', "Select where export const publishToExistingServer = localize('publishToExistingServer', "Publish to existing server"); export const publishToDockerContainer = localize('publishToDockerContainer', "Publish to new server in a container"); export const enterPortNumber = localize('enterPortNumber', "Enter SQL server port number or press enter to use the default value"); +export const serverPortNumber = localize('serverPortNumber', "SQL server port number"); +export const serverPassword = localize('serverPassword', "SQL Server admin password"); +export const confirmServerPassword = localize('confirmServerPassword', "Confirm SQL Server admin password"); +export const baseDockerImage = localize('baseDockerImage', "Base SQL Server Docker image"); +export const publishTo = localize('publishTo', "Publish Target"); export const enterConnectionStringEnvName = localize('enterConnectionStringEnvName', "Enter connection string environment variable name"); export const enterConnectionStringTemplate = localize('enterConnectionStringTemplate', "Enter connection string template"); export const enterPassword = localize('enterPassword', "Enter SQL Server admin password"); @@ -153,6 +158,8 @@ export const mssqlFolderName = '.mssql'; export const dockerFileName = 'Dockerfile'; export const startCommandName = 'start.sh'; export const defaultPortNumber = '1433'; +export const defaultLocalServerName = 'localhost'; +export const defaultLocalServerAdminName = 'sa'; export const defaultConnectionStringEnvVarName = 'SQLConnectionString'; export const defaultConnectionStringTemplate = 'Data Source=@@SERVER@@,@@PORT@@;Initial Catalog=@@DATABASE@@;User id=@@USER@@;Password=@@SA_PASSWORD@@;'; export const azureFunctionLocalSettingsFileName = 'local.settings.json'; diff --git a/extensions/sql-database-projects/src/controllers/projectController.ts b/extensions/sql-database-projects/src/controllers/projectController.ts index 23c30015f3..ec3da0d5ce 100644 --- a/extensions/sql-database-projects/src/controllers/projectController.ts +++ b/extensions/sql-database-projects/src/controllers/projectController.ts @@ -35,13 +35,14 @@ import { CreateProjectFromDatabaseDialog } from '../dialogs/createProjectFromDat import { TelemetryActions, TelemetryReporter, TelemetryViews } from '../common/telemetry'; import { IconPathHelper } from '../common/iconHelper'; import { DashboardData, PublishData, Status } from '../models/dashboardData/dashboardData'; -import { launchPublishDatabaseQuickpick } from '../dialogs/publishDatabaseQuickpick'; +import { getPublishDatabaseSettings, launchPublishTargetOption } from '../dialogs/publishDatabaseQuickpick'; import { launchPublishToDockerContainerQuickpick } from '../dialogs/deployDatabaseQuickpick'; import { DeployService } from '../models/deploy/deployService'; import { SqlTargetPlatform } from 'sqldbproj'; 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'; const maxTableLength = 10; @@ -265,10 +266,9 @@ export class ProjectsController { * Publishes a project to docker container * @param treeNode a treeItem in a project's hierarchy, to be used to obtain a Project */ - public async publishToDockerContainer(context: Project | dataworkspace.WorkspaceTreeItem): Promise { + public async publishToDockerContainer(context: Project | dataworkspace.WorkspaceTreeItem, deployProfile: IDeployProfile): Promise { const project: Project = this.getProjectFromContext(context); try { - let deployProfile = await launchPublishToDockerContainerQuickpick(project); if (deployProfile && deployProfile.deploySettings) { let connectionUri: string | undefined; if (deployProfile.localDbSetting) { @@ -314,6 +314,7 @@ export class ProjectsController { let publishDatabaseDialog = this.getPublishDialog(project); publishDatabaseDialog.publish = async (proj, prof) => this.publishOrScriptProject(proj, prof, true); + publishDatabaseDialog.publishToContainer = async (proj, prof) => this.publishToDockerContainer(proj, prof); publishDatabaseDialog.generateScript = async (proj, prof) => this.publishOrScriptProject(proj, prof, false); publishDatabaseDialog.readPublishProfile = async (profileUri) => readPublishProfile(profileUri); @@ -321,7 +322,39 @@ export class ProjectsController { return publishDatabaseDialog.waitForClose(); } else { - return launchPublishDatabaseQuickpick(project, this); + void this.publishDatabase(project); + } + } + + /** + * Create flow for Publishing a database using only VS Code-native APIs such as QuickPick + */ + private async publishDatabase(project: Project): Promise { + const publishTarget = await launchPublishTargetOption(); + + // Return when user hits escape + if (!publishTarget) { + return undefined; + } + + if (publishTarget === constants.publishToDockerContainer) { + const deployProfile = await launchPublishToDockerContainerQuickpick(project); + if (deployProfile?.deploySettings) { + await this.publishToDockerContainer(project, deployProfile); + } + } else { + let settings: IDeploySettings | undefined = await getPublishDatabaseSettings(project); + + if (settings) { + // 5. Select action to take + const action = await vscode.window.showQuickPick( + [constants.generateScriptButtonText, constants.publish], + { title: constants.chooseAction, ignoreFocusOut: true }); + if (!action) { + return; + } + await this.publishOrScriptProject(project, settings, action === constants.publish); + } } } diff --git a/extensions/sql-database-projects/src/dialogs/deployDatabaseQuickpick.ts b/extensions/sql-database-projects/src/dialogs/deployDatabaseQuickpick.ts index 41293a36aa..f8e9fa081b 100644 --- a/extensions/sql-database-projects/src/dialogs/deployDatabaseQuickpick.ts +++ b/extensions/sql-database-projects/src/dialogs/deployDatabaseQuickpick.ts @@ -6,6 +6,7 @@ import * as vscode from 'vscode'; import * as constants from '../common/constants'; import * as utils from '../common/utils'; +import * as uiUtils from './utils'; import { AppSettingType, IDeployAppIntegrationProfile, IDeployProfile, ILocalDbSetting } from '../models/deploy/deployProfile'; import { Project } from '../models/project'; import { getPublishDatabaseSettings } from './publishDatabaseQuickpick'; @@ -120,11 +121,7 @@ export async function launchPublishToDockerContainerQuickpick(project: Project): } const baseImage = await vscode.window.showQuickPick( - [ - `${constants.sqlServerDockerRegistry}/${constants.sqlServerDockerRepository}:2017-latest`, - `${constants.sqlServerDockerRegistry}/${constants.sqlServerDockerRepository}:2019-latest`, - `${constants.sqlServerDockerRegistry}/${constants.azureSqlEdgeDockerRepository}:latest` - ], + uiUtils.getDockerBaseImages(), { title: constants.selectBaseImage, ignoreFocusOut: true }); // Return when user hits escape @@ -133,8 +130,8 @@ export async function launchPublishToDockerContainerQuickpick(project: Project): } localDbSetting = { - serverName: 'localhost', - userName: 'sa', + serverName: constants.defaultLocalServerName, + userName: constants.defaultLocalServerAdminName, dbName: project.projectFileName, password: password, port: +portNumber, diff --git a/extensions/sql-database-projects/src/dialogs/publishDatabaseDialog.ts b/extensions/sql-database-projects/src/dialogs/publishDatabaseDialog.ts index f402b76644..09d4d4e335 100644 --- a/extensions/sql-database-projects/src/dialogs/publishDatabaseDialog.ts +++ b/extensions/sql-database-projects/src/dialogs/publishDatabaseDialog.ts @@ -14,8 +14,9 @@ import { IDeploySettings } from '../models/IDeploySettings'; import { DeploymentOptions } from '../../../mssql/src/mssql'; import { IconPathHelper } from '../common/iconHelper'; import { cssStyles } from '../common/uiConstants'; -import { getConnectionName } from './utils'; +import { getConnectionName, getDockerBaseImages } from './utils'; import { TelemetryActions, TelemetryReporter, TelemetryViews } from '../common/telemetry'; +import { IDeployProfile } from '../models/deploy/deployProfile'; import { Deferred } from '../common/promise'; interface DataSourceDropdownValue extends azdataType.CategoryValue { @@ -30,14 +31,24 @@ export class PublishDatabaseDialog { private dataSourcesFormComponent: azdataType.FormComponent | undefined; private dataSourcesDropDown: azdataType.DropDownComponent | undefined; private targetDatabaseDropDown: azdataType.DropDownComponent | undefined; + private targetDatabaseTextBox: azdataType.TextComponent | undefined; private connectionsRadioButton: azdataType.RadioButtonComponent | undefined; + private existingServerRadioButton: azdataType.RadioButtonComponent | undefined; + private dockerServerRadioButton: azdataType.RadioButtonComponent | undefined; private dataSourcesRadioButton: azdataType.RadioButtonComponent | undefined; private sqlCmdVariablesTable: azdataType.DeclarativeTableComponent | undefined; private sqlCmdVariablesFormComponentGroup: azdataType.FormComponentGroup | undefined; private loadSqlCmdVarsButton: azdataType.ButtonComponent | undefined; private loadProfileTextBox: azdataType.InputBoxComponent | undefined; private formBuilder: azdataType.FormBuilder | undefined; - + private connectionRow: azdataType.FlexContainer | undefined; + private databaseRow: azdataType.FlexContainer | undefined; + private localDbSection: azdataType.FlexContainer | undefined; + private baseDockerImageDropDown: azdataType.DropDownComponent | undefined; + private serverAdminPasswordTextBox: azdataType.InputBoxComponent | undefined; + private serverConfigAdminPasswordTextBox: azdataType.InputBoxComponent | undefined; + private serverPortTextBox: azdataType.InputBoxComponent | undefined; + private existingServerSelected: boolean = true; private connectionId: string | undefined; private connectionIsDataSource: boolean | undefined; private sqlCmdVars: Record | undefined; @@ -50,6 +61,7 @@ export class PublishDatabaseDialog { private toDispose: vscode.Disposable[] = []; public publish: ((proj: Project, profile: IDeploySettings) => any) | undefined; + public publishToContainer: ((proj: Project, profile: IDeployProfile) => any) | undefined; public generateScript: ((proj: Project, profile: IDeploySettings) => any) | undefined; public readPublishProfile: ((profileUri: vscode.Uri) => any) | undefined; @@ -77,6 +89,11 @@ export class PublishDatabaseDialog { utils.getAzdataApi()!.window.openDialog(this.dialog); } + public set publishToExistingServer(v: boolean) { + this.existingServerSelected = v; + } + + public waitForClose(): Promise { return this.completionPromise.promise; } @@ -92,8 +109,10 @@ export class PublishDatabaseDialog { private initializePublishTab(): void { this.publishTab.registerContent(async view => { + const flexRadioButtonsModel = this.createPublishTypeRadioButtons(view); // TODO : enable using this when data source creation is enabled this.createRadioButtons(view); + this.createLocalDbInfoRow(view); this.dataSourcesFormComponent = this.createDataSourcesFormComponent(view); @@ -115,11 +134,11 @@ export class PublishDatabaseDialog { }; const profileRow = this.createProfileRow(view); - const connectionRow = this.createConnectionRow(view); - const databaseRow = this.createDatabaseRow(view); + this.connectionRow = this.createConnectionRow(view); + this.databaseRow = this.createDatabaseRow(view); const horizontalFormSection = view.modelBuilder.flexContainer().withLayout({ flexFlow: 'column' }).component(); - horizontalFormSection.addItems([profileRow, connectionRow, databaseRow]); + horizontalFormSection.addItems([profileRow, this.databaseRow]); this.formBuilder = view.modelBuilder.formContainer() @@ -127,6 +146,14 @@ export class PublishDatabaseDialog { { title: '', components: [ + { + component: flexRadioButtonsModel, + title: '' + }, + { + component: this.connectionRow, + title: '' + }, { component: horizontalFormSection, title: '' @@ -190,17 +217,41 @@ export class PublishDatabaseDialog { } public async publishClick(): Promise { - const settings: IDeploySettings = { - databaseName: this.getTargetDatabaseName(), - serverName: this.getServerName(), - connectionUri: await this.getConnectionUri(), - sqlCmdVariables: this.getSqlCmdVariablesForPublish(), - deploymentOptions: await this.getDeploymentOptions(), - profileUsed: this.profileUsed - }; + if (this.existingServerSelected) { + const settings: IDeploySettings = { + databaseName: this.targetDatabaseName, + serverName: this.getServerName(), + connectionUri: await this.getConnectionUri(), + sqlCmdVariables: this.getSqlCmdVariablesForPublish(), + deploymentOptions: await this.getDeploymentOptions(), + profileUsed: this.profileUsed + }; - utils.getAzdataApi()!.window.closeDialog(this.dialog); - await this.publish!(this.project, settings); + utils.getAzdataApi()!.window.closeDialog(this.dialog); + await this.publish!(this.project, settings); + } else { + const settings: IDeployProfile = { + localDbSetting: { + dbName: this.targetDatabaseName, + dockerBaseImage: this.getBaseDockerImageName(), + password: this.serverAdminPasswordTextBox?.value || '', + port: +(this.serverPortTextBox?.value || constants.defaultPortNumber), + serverName: constants.defaultLocalServerName, + userName: constants.defaultLocalServerAdminName + }, + deploySettings: { + databaseName: this.targetDatabaseName, + serverName: constants.defaultLocalServerName, + connectionUri: '', + sqlCmdVariables: this.getSqlCmdVariablesForPublish(), + deploymentOptions: await this.getDeploymentOptions(), + profileUsed: this.profileUsed + } + }; + + utils.getAzdataApi()!.window.closeDialog(this.dialog); + await this.publishToContainer!(this.project, settings); + } this.dispose(); } @@ -210,7 +261,7 @@ export class PublishDatabaseDialog { const sqlCmdVars = this.getSqlCmdVariablesForPublish(); const settings: IDeploySettings = { - databaseName: this.getTargetDatabaseName(), + databaseName: this.targetDatabaseName, serverName: this.getServerName(), connectionUri: await this.getConnectionUri(), sqlCmdVariables: sqlCmdVars, @@ -242,8 +293,24 @@ export class PublishDatabaseDialog { return sqlCmdVariables; } - public getTargetDatabaseName(): string { - return this.targetDatabaseDropDown?.value ?? ''; + public get targetDatabaseName(): string { + if (this.existingServerSelected) { + return this.targetDatabaseDropDown?.value ?? ''; + } else { + return this.targetDatabaseTextBox?.value || ''; + } + } + + public set targetDatabaseName(value: string) { + (this.targetDatabaseDropDown).values = []; + this.targetDatabaseDropDown!.values?.push(value); + this.targetDatabaseDropDown!.value = value; + + this.targetDatabaseTextBox!.value = value; + } + + public getBaseDockerImageName(): string { + return this.baseDockerImageDropDown?.value ?? ''; } public getDefaultDatabaseName(): string { @@ -294,6 +361,73 @@ export class PublishDatabaseDialog { return flexRadioButtonsModel; } + private createPublishTypeRadioButtons(view: azdataType.ModelView): azdataType.Component { + const publishToLabel = view.modelBuilder.text().withProps({ + value: constants.publishTo + }).component(); + this.existingServerRadioButton = view.modelBuilder.radioButton() + .withProps({ + name: 'publishType', + label: constants.publishToExistingServer + }).component(); + + this.existingServerRadioButton.checked = true; + this.existingServerRadioButton.onDidChangeCheckedState((checked) => { + this.onPublishTypeChange(checked, view); + }); + + this.dockerServerRadioButton = view.modelBuilder.radioButton() + .withProps({ + name: 'publishType', + label: constants.publishToDockerContainer + }).component(); + + this.dockerServerRadioButton.onDidChangeCheckedState((checked) => { + this.onPublishTypeChange(!checked, view); + }); + + let flexRadioButtonsModel: azdataType.FlexContainer = view.modelBuilder.flexContainer() + .withLayout({ flexFlow: 'column' }) + .withItems([publishToLabel, this.existingServerRadioButton, this.dockerServerRadioButton]) + .withProps({ ariaRole: 'radiogroup' }) + .component(); + + return flexRadioButtonsModel; + } + + private onPublishTypeChange(existingServer: boolean, view: azdataType.ModelView) { + this.existingServerSelected = existingServer; + this.createDatabaseRow(view); + this.tryEnableGenerateScriptAndOkButtons(); + if (existingServer) { + if (this.connectionRow) { + this.formBuilder!.insertFormItem({ + title: '', + component: this.connectionRow + }, 2); + } + if (this.localDbSection) { + this.formBuilder!.removeFormItem({ + title: '', + component: this.localDbSection + }); + } + } else { + if (this.connectionRow) { + this.formBuilder!.removeFormItem({ + title: '', + component: this.connectionRow + }); + } + if (this.localDbSection) { + this.formBuilder!.insertFormItem({ + title: '', + component: this.localDbSection + }, 2); + } + } + } + private createTargetConnectionComponent(view: azdataType.ModelView): azdataType.InputBoxComponent { this.targetConnectionTextBox = view.modelBuilder.inputBox().withProps({ value: '', @@ -393,30 +527,124 @@ export class PublishDatabaseDialog { return connectionRow; } - private createDatabaseRow(view: azdataType.ModelView): azdataType.FlexContainer { - this.targetDatabaseDropDown = view.modelBuilder.dropDown().withProps({ - values: [this.getDefaultDatabaseName()], - value: this.getDefaultDatabaseName(), - ariaLabel: constants.databaseNameLabel, - required: true, + private createLocalDbInfoRow(view: azdataType.ModelView): azdataType.FlexContainer { + this.serverPortTextBox = view.modelBuilder.inputBox().withProps({ + value: constants.defaultPortNumber, + ariaLabel: constants.serverPortNumber, + placeHolder: constants.serverPortNumber, width: cssStyles.publishDialogTextboxWidth, - editable: true, - fireOnTextChange: true - }).component(); + enabled: true, + inputType: 'number', + validationErrorMessage: constants.portMustBeNumber + }).withValidation(component => utils.validateSqlServerPortNumber(component.value)).component(); - this.targetDatabaseDropDown.onValueChanged(() => { + this.serverPortTextBox.onTextChanged(() => { this.tryEnableGenerateScriptAndOkButtons(); }); + const serverPortRow = this.createFormRow(view, constants.serverPortNumber, this.serverPortTextBox); + this.serverAdminPasswordTextBox = view.modelBuilder.inputBox().withProps({ + value: '', + ariaLabel: constants.serverPassword, + placeHolder: constants.serverPassword, + width: cssStyles.publishDialogTextboxWidth, + enabled: true, + inputType: 'password', + validationErrorMessage: constants.invalidSQLPasswordMessage + }).withValidation(component => !utils.isEmptyString(component.value) && utils.isValidSQLPassword(component.value || '')).component(); + + const serverPasswordRow = this.createFormRow(view, constants.serverPassword, this.serverAdminPasswordTextBox); + this.serverConfigAdminPasswordTextBox = view.modelBuilder.inputBox().withProps({ + value: '', + ariaLabel: constants.confirmServerPassword, + placeHolder: constants.confirmServerPassword, + width: cssStyles.publishDialogTextboxWidth, + enabled: true, + inputType: 'password', + validationErrorMessage: constants.passwordNotMatch + }).withValidation(component => component.value === this.serverAdminPasswordTextBox?.value).component(); + this.serverAdminPasswordTextBox.onTextChanged(() => { + this.tryEnableGenerateScriptAndOkButtons(); + if (this.serverConfigAdminPasswordTextBox) { + this.serverConfigAdminPasswordTextBox.value = ''; + } + }); + this.serverConfigAdminPasswordTextBox.onTextChanged(() => { + this.tryEnableGenerateScriptAndOkButtons(); + + }); + const serverConfirmPasswordRow = this.createFormRow(view, constants.confirmServerPassword, this.serverConfigAdminPasswordTextBox); + + this.baseDockerImageDropDown = view.modelBuilder.dropDown().withProps({ + values: getDockerBaseImages(), + ariaLabel: constants.baseDockerImage, + width: cssStyles.publishDialogTextboxWidth, + enabled: true + }).component(); + + const dropDownRow = this.createFormRow(view, constants.baseDockerImage, this.baseDockerImageDropDown); + + this.localDbSection = view.modelBuilder.flexContainer().withLayout({ flexFlow: 'column' }).component(); + this.localDbSection.addItems([serverPortRow, serverPasswordRow, serverConfirmPasswordRow, dropDownRow]); + return this.localDbSection; + } + + private createFormRow(view: azdataType.ModelView, label: string, component: azdataType.Component): azdataType.FlexContainer { + + const labelComponent = view.modelBuilder.text().withProps({ + value: label, + requiredIndicator: true, + width: cssStyles.publishDialogLabelWidth + }).component(); + + return view.modelBuilder.flexContainer().withItems([labelComponent, component], { flex: '0 0 auto', CSSStyles: { 'margin-right': '10px' } }).withLayout({ flexFlow: 'row', alignItems: 'center' }).component(); + } + + private createDatabaseRow(view: azdataType.ModelView): azdataType.FlexContainer { + let databaseComponent: azdataType.Component | undefined; + + if (!this.existingServerSelected) { + if (this.targetDatabaseTextBox === undefined) { + this.targetDatabaseTextBox = view.modelBuilder.inputBox().withProps({ + ariaLabel: constants.databaseNameLabel, + required: true, + width: cssStyles.publishDialogTextboxWidth, + value: this.getDefaultDatabaseName() + }).component(); + } + databaseComponent = this.targetDatabaseTextBox; + } else { + if (this.targetDatabaseDropDown === undefined) { + this.targetDatabaseDropDown = view.modelBuilder.dropDown().withProps({ + values: [this.getDefaultDatabaseName()], + value: this.getDefaultDatabaseName(), + ariaLabel: constants.databaseNameLabel, + required: true, + width: cssStyles.publishDialogTextboxWidth, + editable: true, + fireOnTextChange: true + }).component(); + + this.targetDatabaseDropDown.onValueChanged(() => { + this.tryEnableGenerateScriptAndOkButtons(); + }); + } + + databaseComponent = this.targetDatabaseDropDown; + } const databaseLabel = view.modelBuilder.text().withProps({ value: constants.databaseNameLabel, requiredIndicator: true, width: cssStyles.publishDialogLabelWidth }).component(); - - const databaseRow = view.modelBuilder.flexContainer().withItems([databaseLabel, this.targetDatabaseDropDown], { flex: '0 0 auto', CSSStyles: { 'margin-right': '10px' } }).withLayout({ flexFlow: 'row', alignItems: 'center' }).component(); - - return databaseRow; + const itemLayout = { flex: '0 0 auto', CSSStyles: { 'margin-right': '10px' } }; + if (this.databaseRow === undefined) { + this.databaseRow = view.modelBuilder.flexContainer().withItems([databaseLabel, databaseComponent], itemLayout).withLayout({ flexFlow: 'row', alignItems: 'center' }).component(); + } else { + this.databaseRow.clearItems(); + this.databaseRow.addItems([databaseLabel, databaseComponent], itemLayout); + } + return this.databaseRow; } private createSqlCmdTable(view: azdataType.ModelView): azdataType.DeclarativeTableComponent { @@ -543,15 +771,14 @@ export class PublishDatabaseDialog { if (this.readPublishProfile) { const result = await this.readPublishProfile(fileUris[0]); // clear out old database dropdown values. They'll get populated later if there was a connection specified in the profile - (this.targetDatabaseDropDown).values = []; + this.targetDatabaseName = ''; this.connectionId = result.connectionId; this.serverName = result.serverName; await this.updateConnectionComponents(result.connection, this.connectionId); if (result.databaseName) { - this.targetDatabaseDropDown!.values?.push(result.databaseName); - this.targetDatabaseDropDown!.value = result.databaseName; + this.targetDatabaseName = result.databaseName; } if (Object.keys(result.sqlCmdVariables).length) { @@ -595,15 +822,25 @@ export class PublishDatabaseDialog { // only enable Generate Script and Ok buttons if all fields are filled private tryEnableGenerateScriptAndOkButtons(): void { - if ((this.targetConnectionTextBox!.value && this.targetDatabaseDropDown!.value - || this.connectionIsDataSource && this.targetDatabaseDropDown!.value) - && this.allSqlCmdVariablesFilled()) { - this.dialog.okButton.enabled = true; - this.dialog.customButtons[0].enabled = true; - } else { - this.dialog.okButton.enabled = false; - this.dialog.customButtons[0].enabled = false; + let publishEnabled: boolean = false; + let generateScriptEnabled: boolean = false; + + if (this.existingServerRadioButton?.checked) { + if ((this.targetConnectionTextBox!.value && this.targetDatabaseDropDown!.value + || this.connectionIsDataSource && this.targetDatabaseDropDown!.value) + && this.allSqlCmdVariablesFilled()) { + publishEnabled = true; + generateScriptEnabled = true; + } + } else if (utils.validateSqlServerPortNumber(this.serverPortTextBox?.value) && + !utils.isEmptyString(this.serverAdminPasswordTextBox?.value) && + utils.isValidSQLPassword(this.serverAdminPasswordTextBox?.value || '', constants.defaultLocalServerAdminName) && + this.serverAdminPasswordTextBox?.value === this.serverConfigAdminPasswordTextBox?.value) { + publishEnabled = true; // only publish is supported for container } + + this.dialog.okButton.enabled = publishEnabled; + this.dialog.customButtons[0].enabled = generateScriptEnabled; } private allSqlCmdVariablesFilled(): boolean { diff --git a/extensions/sql-database-projects/src/dialogs/publishDatabaseQuickpick.ts b/extensions/sql-database-projects/src/dialogs/publishDatabaseQuickpick.ts index 8a739edad9..a4d09fe7c4 100644 --- a/extensions/sql-database-projects/src/dialogs/publishDatabaseQuickpick.ts +++ b/extensions/sql-database-projects/src/dialogs/publishDatabaseQuickpick.ts @@ -10,7 +10,6 @@ import { PublishProfile, readPublishProfile } from '../models/publishProfile/pub import { promptForPublishProfile } from './publishDatabaseDialog'; import { getDefaultPublishDeploymentOptions, getVscodeMssqlApi } from '../common/utils'; import { IConnectionInfo } from 'vscode-mssql'; -import { ProjectsController } from '../controllers/projectController'; import { IDeploySettings } from '../models/IDeploySettings'; /** @@ -203,36 +202,7 @@ export async function getPublishDatabaseSettings(project: Project, promptForConn return settings; } -/** -* Create flow for Publishing a database using only VS Code-native APIs such as QuickPick -*/ -export async function launchPublishDatabaseQuickpick(project: Project, projectController: ProjectsController): Promise { - const publishTarget = await launchPublishTargetOption(); - - // Return when user hits escape - if (!publishTarget) { - return undefined; - } - - if (publishTarget === constants.publishToDockerContainer) { - await projectController.publishToDockerContainer(project); - } else { - let settings: IDeploySettings | undefined = await getPublishDatabaseSettings(project); - - if (settings) { - // 5. Select action to take - const action = await vscode.window.showQuickPick( - [constants.generateScriptButtonText, constants.publish], - { title: constants.chooseAction, ignoreFocusOut: true }); - if (!action) { - return; - } - await projectController.publishOrScriptProject(project, settings, action === constants.publish); - } - } -} - -async function launchPublishTargetOption(): Promise { +export async function launchPublishTargetOption(): Promise { // Show options to user for deploy to existing server or docker const publishOption = await vscode.window.showQuickPick( diff --git a/extensions/sql-database-projects/src/dialogs/utils.ts b/extensions/sql-database-projects/src/dialogs/utils.ts index 78f27cce3b..6d9d80cca1 100644 --- a/extensions/sql-database-projects/src/dialogs/utils.ts +++ b/extensions/sql-database-projects/src/dialogs/utils.ts @@ -24,3 +24,12 @@ export function getConnectionName(connection: any): string { return connectionName; } + + +export function getDockerBaseImages(): string[] { + return [ + `${constants.sqlServerDockerRegistry}/${constants.sqlServerDockerRepository}:2017-latest`, + `${constants.sqlServerDockerRegistry}/${constants.sqlServerDockerRepository}:2019-latest`, + `${constants.sqlServerDockerRegistry}/${constants.azureSqlEdgeDockerRepository}:latest` + ]; +} diff --git a/extensions/sql-database-projects/src/models/deploy/deployService.ts b/extensions/sql-database-projects/src/models/deploy/deployService.ts index 5110e96115..f6039e83ea 100644 --- a/extensions/sql-database-projects/src/models/deploy/deployService.ts +++ b/extensions/sql-database-projects/src/models/deploy/deployService.ts @@ -245,7 +245,7 @@ export class DeployService { options: [], authenticationType: 'SqlLogin' }; - return await getAzdataApi.connection.connect(connectionProfile, false, false); + return await getAzdataApi.connection.connect(connectionProfile, saveConnectionAndPassword, false); } else if (vscodeMssqlApi) { const connectionProfile = { password: profile.password, 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 f323333beb..914c0f2fa6 100644 --- a/extensions/sql-database-projects/src/test/dialogs/publishDatabaseDialog.test.ts +++ b/extensions/sql-database-projects/src/test/dialogs/publishDatabaseDialog.test.ts @@ -18,6 +18,7 @@ import { ProjectsController } from '../../controllers/projectController'; import { IDeploySettings } from '../../models/IDeploySettings'; import { emptySqlDatabaseProjectTypeId } from '../../common/constants'; import { createContext, mockDacFxOptionsResult, TestContext } from '../testContext'; +import { IDeployProfile } from '../../models/deploy/deployProfile'; let testContext: TestContext; describe('Publish Database Dialog', () => { @@ -66,10 +67,11 @@ describe('Publish Database Dialog', () => { const proj = await testUtils.createTestProject(baselines.openProjectFileBaseline); const dialog = TypeMoq.Mock.ofType(PublishDatabaseDialog, undefined, undefined, proj); dialog.setup(x => x.getConnectionUri()).returns(() => { return Promise.resolve('Mock|Connection|Uri'); }); - dialog.setup(x => x.getTargetDatabaseName()).returns(() => 'MockDatabaseName'); + dialog.setup(x => x.targetDatabaseName).returns(() => 'MockDatabaseName'); dialog.setup(x => x.getSqlCmdVariablesForPublish()).returns(() => proj.sqlCmdVariables); dialog.setup(x => x.getDeploymentOptions()).returns(() => { return Promise.resolve(mockDacFxOptionsResult.deploymentOptions); }); dialog.setup(x => x.getServerName()).returns(() => 'MockServer'); + dialog.object.publishToExistingServer = true; dialog.callBase = true; let profile: IDeploySettings | undefined; @@ -107,5 +109,34 @@ describe('Publish Database Dialog', () => { await dialog.object.generateScriptClick(); should(profile).deepEqual(expectedGenScript); + + const expectedContainerPublishProfile: IDeployProfile = { + localDbSetting: { + dbName: 'MockDatabaseName', + dockerBaseImage: '', + password: '', + port: 1433, + serverName: 'localhost', + userName: 'sa' + + }, + deploySettings: { + databaseName: 'MockDatabaseName', + serverName: 'localhost', + connectionUri: '', + sqlCmdVariables: { + 'ProdDatabaseName': 'MyProdDatabase', + 'BackupDatabaseName': 'MyBackupDatabase' + }, + deploymentOptions: mockDacFxOptionsResult.deploymentOptions, + profileUsed: false + } + }; + dialog.object.publishToExistingServer = false; + let deployProfile: IDeployProfile | undefined; + dialog.object.publishToContainer = (_, prof) => { deployProfile = prof; }; + await dialog.object.publishClick(); + + should(deployProfile).deepEqual(expectedContainerPublishProfile); }); }); diff --git a/extensions/sql-database-projects/src/test/projectController.test.ts b/extensions/sql-database-projects/src/test/projectController.test.ts index 61b68670b7..6011802b9c 100644 --- a/extensions/sql-database-projects/src/test/projectController.test.ts +++ b/extensions/sql-database-projects/src/test/projectController.test.ts @@ -395,7 +395,7 @@ describe('ProjectsController', function (): void { holler = generateHoller; return Promise.resolve(undefined); }); - + publishDialog.object.publishToExistingServer = true; void projController.object.publishProject(proj); await publishDialog.object.publishClick();