/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import type * as azdataType from 'azdata'; import * as vscode from 'vscode'; import * as constants from '../common/constants'; import * as utils from '../common/utils'; import * as uiUtils from './utils'; import * as path from 'path'; import { Project } from '../models/project'; import { SqlConnectionDataSource } from '../models/dataSources/sqlConnectionStringSource'; import { DeploymentOptions } from 'mssql'; import { IconPathHelper } from '../common/iconHelper'; import { cssStyles } from '../common/uiConstants'; import { getAgreementDisplayText, getConnectionName, getDockerBaseImages, getPublishServerName } from './utils'; import { TelemetryActions, TelemetryReporter, TelemetryViews } from '../common/telemetry'; import { Deferred } from '../common/promise'; import { PublishOptionsDialog } from './publishOptionsDialog'; import { IPublishToDockerSettings, ISqlProjectPublishSettings } from '../models/deploy/publishSettings'; import { PublishProfile } from '../models/publishProfile/publishProfile'; interface DataSourceDropdownValue extends azdataType.CategoryValue { dataSource: SqlConnectionDataSource; database: string; } export class PublishDatabaseDialog { public dialog: azdataType.window.Dialog; public publishTab: azdataType.window.DialogTab; private targetConnectionTextBox: azdataType.InputBoxComponent | undefined; private dataSourcesDropDown: azdataType.DropDownComponent | undefined; private targetDatabaseDropDown: azdataType.DropDownComponent | undefined; private targetDatabaseTextBox: azdataType.TextComponent | undefined; private selectConnectionButton: azdataType.ButtonComponent | undefined; private existingServerRadioButton: azdataType.RadioButtonComponent | undefined; private dockerServerRadioButton: azdataType.RadioButtonComponent | undefined; private eulaCheckBox: azdataType.CheckBoxComponent | undefined; private sqlCmdVariablesTable: azdataType.DeclarativeTableComponent | undefined; private sqlCmdVariablesFormComponentGroup: azdataType.FormComponentGroup | undefined; private revertSqlCmdVarsButton: 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 imageTagDropDown: 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: Map | undefined; private deploymentOptions: DeploymentOptions | undefined; private profileUsed: boolean = false; private serverName: string | undefined; protected optionsButton: azdataType.ButtonComponent | undefined; private publishOptionsDialog: PublishOptionsDialog | undefined; public publishOptionsModified: boolean = false; private publishProfileUri: vscode.Uri | undefined; private completionPromise: Deferred = new Deferred(); private toDispose: vscode.Disposable[] = []; public publish: ((proj: Project, profile: ISqlProjectPublishSettings) => any) | undefined; public publishToContainer: ((proj: Project, profile: IPublishToDockerSettings) => any) | undefined; public generateScript: ((proj: Project, profile: ISqlProjectPublishSettings) => any) | undefined; public readPublishProfile: ((profileUri: vscode.Uri) => Promise) | undefined; public savePublishProfile: ((profilePath: string, databaseName: string, connectionString: string, sqlCommandVariableValues?: Map, deploymentOptions?: DeploymentOptions) => any) | undefined; constructor(private project: Project) { this.dialog = utils.getAzdataApi()!.window.createModelViewDialog(constants.publishDialogName, 'sqlProjectPublishDialog'); this.toDispose.push(this.dialog.onClosed(_ => this.completionPromise.resolve())); this.publishTab = utils.getAzdataApi()!.window.createTab(constants.publishDialogName); } public openDialog(): void { this.initializeDialog(); this.dialog.okButton.label = constants.publish; this.dialog.okButton.enabled = false; this.toDispose.push(this.dialog.okButton.onClick(async () => await this.publishClick())); this.dialog.cancelButton.label = constants.cancelButtonText; let generateScriptButton: azdataType.window.Button = utils.getAzdataApi()!.window.createButton(constants.generateScriptButtonText); this.toDispose.push(generateScriptButton.onClick(async () => await this.generateScriptClick())); generateScriptButton.enabled = false; this.dialog.customButtons = []; this.dialog.customButtons.push(generateScriptButton); utils.getAzdataApi()!.window.openDialog(this.dialog); } public set publishToExistingServer(v: boolean) { this.existingServerSelected = v; } public waitForClose(): Promise { return this.completionPromise.promise; } private dispose(): void { this.toDispose.forEach(disposable => disposable.dispose()); } private initializeDialog(): void { this.initializePublishTab(); this.dialog.content = [this.publishTab]; } private initializePublishTab(): void { this.publishTab.registerContent(async view => { const flexRadioButtonsModel = this.createPublishTypeRadioButtons(view); await this.createLocalDbInfoRow(view); this.sqlCmdVariablesTable = this.createSqlCmdTable(view); this.revertSqlCmdVarsButton = this.createRevertSqlCmdVarsButton(view); this.sqlCmdVariablesFormComponentGroup = { components: [ { title: '', component: this.revertSqlCmdVarsButton }, { title: '', component: this.sqlCmdVariablesTable } ], title: constants.sqlCmdVariables }; // Get the default deployment option and set const options = await this.getDefaultDeploymentOptions(); this.setDeploymentOptions(options); const profileRow = this.createProfileSection(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([this.databaseRow]); this.formBuilder = view.modelBuilder.formContainer() .withFormItems([ { title: '', components: [ { component: flexRadioButtonsModel, title: '' }, { component: profileRow, title: constants.profile }, { component: this.connectionRow, title: '' }, { component: horizontalFormSection, title: '' }, /* TODO : enable using this when data source creation is enabled { title: constants.selectConnectionRadioButtonsTitle, component: selectConnectionRadioButtons },*/ { component: displayOptionsButton, title: '' } ] } ], { horizontal: false, titleFontSize: cssStyles.titleFontSize }) .withLayout({ width: '100%' }); // add SQLCMD variables table if the project has any if (this.project.sqlCmdVariables.size > 0) { this.formBuilder.addFormItem(this.sqlCmdVariablesFormComponentGroup); } let formModel = this.formBuilder.component(); await view.initializeModel(formModel); }); } public async getConnectionUri(): Promise { try { // if target connection is a data source, have to check if already connected or if connection dialog needs to be opened let connId: string; if (this.connectionIsDataSource) { const dataSource = (this.dataSourcesDropDown!.value! as DataSourceDropdownValue).dataSource; const connProfile: azdataType.IConnectionProfile = dataSource.getConnectionProfile(); if (dataSource.integratedSecurity) { const connResult = await utils.getAzdataApi()!.connection.connect(connProfile, false, false); utils.throwIfNotConnected(connResult); connId = connResult.connectionId!; } else { connId = (await utils.getAzdataApi()!.connection.openConnectionDialog(undefined, connProfile)).connectionId; } } else { if (!this.connectionId) { throw new Error('Connection not defined.'); } connId = this.connectionId; } return await utils.getAzdataApi()!.connection.getUriForConnection(connId); } catch (err) { throw new Error(constants.unableToCreatePublishConnection + ': ' + utils.getErrorMessage(err)); } } public async publishClick(): Promise { if (this.existingServerSelected) { const settings: ISqlProjectPublishSettings = { 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); } else { let dockerBaseImage = this.getBaseDockerImageName(); const baseImages = getDockerBaseImages(this.project.getProjectTargetVersion()); const imageInfo = baseImages.find(x => x.name === dockerBaseImage); const imageName = imageInfo?.name; const imageTag = this.imageTagDropDown?.value; // Add the image tag if it's not the latest if (imageTag && imageTag !== constants.dockerImageDefaultTag) { dockerBaseImage = `${imageName}:${imageTag}`; } const settings: IPublishToDockerSettings = { dockerSettings: { dbName: this.targetDatabaseName, dockerBaseImage: dockerBaseImage, dockerBaseImageEula: imageInfo?.agreementInfo?.link?.url || '', password: this.serverAdminPasswordTextBox?.value || '', port: +(this.serverPortTextBox?.value || constants.defaultPortNumber), serverName: constants.defaultLocalServerName, userName: constants.defaultLocalServerAdminName }, sqlProjectPublishSettings: { 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(); } public async generateScriptClick(): Promise { TelemetryReporter.sendActionEvent(TelemetryViews.SqlProjectPublishDialog, TelemetryActions.generateScriptClicked); const sqlCmdVars = this.getSqlCmdVariablesForPublish(); const settings: ISqlProjectPublishSettings = { databaseName: this.targetDatabaseName, serverName: this.getServerName(), connectionUri: await this.getConnectionUri(), sqlCmdVariables: sqlCmdVars, deploymentOptions: await this.getDeploymentOptions(), profileUsed: this.profileUsed }; utils.getAzdataApi()!.window.closeDialog(this.dialog); await this.generateScript?.(this.project, settings); this.dispose(); } public async getDeploymentOptions(): Promise { 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; } return this.deploymentOptions; } public getSqlCmdVariablesForPublish(): Map { // get SQLCMD variables from table let sqlCmdVariables = this.sqlCmdVars ?? new Map(); return sqlCmdVariables; } 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; if (this.targetDatabaseTextBox) { this.targetDatabaseTextBox!.value = value; } } public getBaseDockerImageName(): string { return (this.baseDockerImageDropDown?.value)?.name ?? ''; } public getDefaultDatabaseName(): string { return this.project.projectFileName; } public getServerName(): string { return this.serverName!; } private createPublishTypeRadioButtons(view: azdataType.ModelView): azdataType.Component { const name = getPublishServerName(this.project.getProjectTargetVersion()); const publishToLabel = view.modelBuilder.text().withProps({ value: constants.publishTo, width: cssStyles.publishDialogLabelWidth }).component(); this.existingServerRadioButton = view.modelBuilder.radioButton() .withProps({ name: 'publishType', label: constants.publishToExistingServer(name) }).component(); this.existingServerRadioButton.checked = true; this.existingServerRadioButton.onDidChangeCheckedState((checked) => { this.onPublishTypeChange(checked, view); }); this.dockerServerRadioButton = view.modelBuilder.radioButton() .withProps({ name: 'publishType', label: name === constants.AzureSqlServerName ? constants.publishToDockerContainerPreview(name) : constants.publishToDockerContainer(name) }).component(); this.dockerServerRadioButton.onDidChangeCheckedState((checked) => { this.onPublishTypeChange(!checked, view); }); const radioButtonContainer = view.modelBuilder.flexContainer() .withLayout({ flexFlow: 'column' }) .withItems([this.existingServerRadioButton, this.dockerServerRadioButton]) .withProps({ ariaRole: 'radiogroup', ariaLabel: constants.publishTo }) .component(); let flexRadioButtonsModel: azdataType.FlexContainer = view.modelBuilder.flexContainer() .withLayout({ flexFlow: 'row', alignItems: 'baseline' }) .withItems([publishToLabel, radioButtonContainer], { CSSStyles: { flex: '0 0 auto', 'margin-right': '10px' } }) .component(); return flexRadioButtonsModel; } private onPublishTypeChange(existingServer: boolean, view: azdataType.ModelView) { this.existingServerSelected = existingServer; this.createDatabaseRow(view); this.tryEnableGenerateScriptAndPublishButtons(); if (existingServer) { if (this.localDbSection) { this.formBuilder!.removeFormItem({ title: '', component: this.localDbSection }); } if (this.connectionRow) { this.formBuilder!.insertFormItem({ title: '', component: this.connectionRow }, 3); } } 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: '', ariaLabel: constants.targetConnectionLabel, placeHolder: constants.selectConnection, width: cssStyles.publishDialogTextboxWidth, enabled: false }).component(); this.targetConnectionTextBox.onTextChanged(() => { this.tryEnableGenerateScriptAndPublishButtons(); }); return this.targetConnectionTextBox; } private createProfileSection(view: azdataType.ModelView): azdataType.FlexContainer { const selectProfileButton = this.createSelectProfileButton(view); const saveProfileAsButton = this.createSaveProfileAsButton(view); this.loadProfileTextBox = view.modelBuilder.inputBox().withProps({ placeHolder: constants.loadProfilePlaceholderText, ariaLabel: constants.profile, width: '200px', enabled: false }).component(); const buttonsList = view.modelBuilder.flexContainer().withItems([selectProfileButton, saveProfileAsButton], { flex: '0 0 auto', CSSStyles: { 'margin-right': '5px', 'text-align': 'justify' } }).withLayout({ flexFlow: 'row', alignItems: 'center' }).component(); const profileRow = view.modelBuilder.flexContainer().withItems([this.loadProfileTextBox, buttonsList], { flex: '0 0 auto', CSSStyles: { 'margin-right': '15px', 'text-align': 'justify' } }).withLayout({ flexFlow: 'row', alignItems: 'center' }).component(); return profileRow; } private createConnectionRow(view: azdataType.ModelView): azdataType.FlexContainer { this.targetConnectionTextBox = this.createTargetConnectionComponent(view); const selectConnectionButton: azdataType.Component = this.createSelectConnectionButton(view); const serverLabel = view.modelBuilder.text().withProps({ value: constants.server, requiredIndicator: true, width: cssStyles.publishDialogLabelWidth }).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; } private async createLocalDbInfoRow(view: azdataType.ModelView): Promise { const name = getPublishServerName(this.project.getProjectTargetVersion()); this.serverPortTextBox = view.modelBuilder.inputBox().withProps({ value: constants.defaultPortNumber, ariaLabel: constants.serverPortNumber(name), placeHolder: constants.serverPortNumber(name), width: cssStyles.publishDialogTextboxWidth, enabled: true, inputType: 'number', validationErrorMessage: constants.portMustBeNumber, required: true }).withValidation(component => utils.validateSqlServerPortNumber(component.value)).component(); this.serverPortTextBox.onTextChanged(() => { this.tryEnableGenerateScriptAndPublishButtons(); }); const serverPortRow = this.createFormRow(view, constants.serverPortNumber(name), this.serverPortTextBox); this.serverAdminPasswordTextBox = view.modelBuilder.inputBox().withProps({ value: '', ariaLabel: constants.serverPassword(name), placeHolder: constants.serverPassword(name), width: cssStyles.publishDialogTextboxWidth, enabled: true, inputType: 'password', validationErrorMessage: constants.invalidSQLPasswordMessage(name), required: true }).withValidation(component => !utils.isEmptyString(component.value) && utils.isValidSQLPassword(component.value || '')).component(); const serverPasswordRow = this.createFormRow(view, constants.serverPassword(name), this.serverAdminPasswordTextBox); this.serverConfigAdminPasswordTextBox = view.modelBuilder.inputBox().withProps({ value: '', ariaLabel: constants.confirmServerPassword(name), placeHolder: constants.confirmServerPassword(name), width: cssStyles.publishDialogTextboxWidth, enabled: true, inputType: 'password', validationErrorMessage: constants.passwordNotMatch(name), required: true }).withValidation(component => component.value === this.serverAdminPasswordTextBox?.value).component(); this.serverAdminPasswordTextBox.onTextChanged(() => { this.tryEnableGenerateScriptAndPublishButtons(); if (this.serverConfigAdminPasswordTextBox) { this.serverConfigAdminPasswordTextBox.value = ''; } }); this.serverConfigAdminPasswordTextBox.onTextChanged(() => { this.tryEnableGenerateScriptAndPublishButtons(); }); const serverConfirmPasswordRow = this.createFormRow(view, constants.confirmServerPassword(name), this.serverConfigAdminPasswordTextBox); const baseImages = getDockerBaseImages(this.project.getProjectTargetVersion()); const baseImagesValues: azdataType.CategoryValue[] = baseImages.map(x => { return { name: x.name, displayName: x.displayName }; }); this.baseDockerImageDropDown = view.modelBuilder.dropDown().withProps({ values: baseImagesValues, ariaLabel: constants.baseDockerImage(name), width: cssStyles.publishDialogTextboxWidth, enabled: true, required: true }).component(); const imageInfo = baseImages.find(x => x.displayName === (this.baseDockerImageDropDown?.value)?.displayName); const imageTags = await uiUtils.getImageTags(imageInfo!, this.project.getProjectTargetVersion(), true); this.imageTagDropDown = view.modelBuilder.dropDown().withProps({ values: imageTags, value: imageTags[0], ariaLabel: constants.imageTag, width: cssStyles.publishDialogTextboxWidth, enabled: true, editable: true, required: true, fireOnTextChange: true }).component(); this.imageTagDropDown.onValueChanged(() => { this.tryEnableGenerateScriptAndPublishButtons(); }); const agreementInfo = baseImages[0].agreementInfo; const baseImageDropDownRow = this.createFormRow(view, constants.baseDockerImage(name), this.baseDockerImageDropDown); const imageTagDropDownRow = this.createFormRow(view, constants.imageTag, this.imageTagDropDown); this.eulaCheckBox = view.modelBuilder.checkBox().withProps({ ariaLabel: getAgreementDisplayText(agreementInfo), required: true }).component(); this.eulaCheckBox.onChanged(() => { this.tryEnableGenerateScriptAndPublishButtons(); }); const eulaRow = view.modelBuilder.flexContainer().withLayout({ flexFlow: 'row', alignItems: 'center' }).component(); this.localDbSection = view.modelBuilder.flexContainer().withLayout({ flexFlow: 'column' }).component(); this.localDbSection.addItems([serverPortRow, serverPasswordRow, serverConfirmPasswordRow, baseImageDropDownRow, imageTagDropDownRow, eulaRow]); this.baseDockerImageDropDown.onValueChanged(async () => { if (this.eulaCheckBox) { this.eulaCheckBox.checked = false; } const baseImage = getDockerBaseImages(this.project.getProjectTargetVersion()).find(x => x.name === (this.baseDockerImageDropDown?.value).name); if (baseImage?.agreementInfo.link) { const text = view.modelBuilder.text().withProps({ value: constants.eulaAgreementTemplate, links: [baseImage.agreementInfo.link], requiredIndicator: true }).component(); if (eulaRow && this.eulaCheckBox) { eulaRow?.clearItems(); eulaRow?.addItems([this.eulaCheckBox, text], { flex: '0 0 auto', CSSStyles: { 'margin-right': '10px' } }); } } // update image tag dropdown with the image tags for the selected base image const imageInfo = baseImages.find(x => x.displayName === baseImage?.displayName); const imageTags = await uiUtils.getImageTags(imageInfo!, this.project.getProjectTargetVersion(), true); this.imageTagDropDown!.values = imageTags; this.imageTagDropDown!.value = imageTags[0]; }); 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.publishDialogDropdownWidth, 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.publishDialogDropdownWidth, editable: true, fireOnTextChange: true }).component(); this.targetDatabaseDropDown.onValueChanged(() => { this.tryEnableGenerateScriptAndPublishButtons(); }); } databaseComponent = this.targetDatabaseDropDown; } const databaseLabel = view.modelBuilder.text().withProps({ value: constants.databaseNameLabel, requiredIndicator: true, width: cssStyles.publishDialogLabelWidth }).component(); 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 { this.sqlCmdVars = this.project.sqlCmdVariables; const table = view.modelBuilder.declarativeTable().withProps({ ariaLabel: constants.sqlCmdVariables, dataValues: this.convertSqlCmdVarsToTableFormat(this.sqlCmdVars), columns: [ { displayName: constants.sqlCmdVariableColumn, valueType: utils.getAzdataApi()!.DeclarativeDataType.string, width: '50%', isReadOnly: true, headerCssStyles: cssStyles.tableHeader, rowCssStyles: cssStyles.tableRow }, { displayName: constants.sqlCmdValueColumn, valueType: utils.getAzdataApi()!.DeclarativeDataType.string, width: '50%', isReadOnly: false, headerCssStyles: cssStyles.tableHeader, rowCssStyles: cssStyles.tableRow }], width: '420px' }).component(); table.onDataChanged(() => { this.sqlCmdVars = new Map(); table.dataValues?.forEach((row) => { this.sqlCmdVars?.set(row[0].value, row[1].value); }); this.updateRevertSqlCmdVarsButtonState(); this.tryEnableGenerateScriptAndPublishButtons(); }); return table; } private createRevertSqlCmdVarsButton(view: azdataType.ModelView): azdataType.ButtonComponent { let loadSqlCmdVarsButton: azdataType.ButtonComponent = view.modelBuilder.button().withProps({ label: constants.revertSqlCmdVarsButtonTitle, title: constants.revertSqlCmdVarsButtonTitle, ariaLabel: constants.revertSqlCmdVarsButtonTitle, width: '210px', iconPath: IconPathHelper.refresh, height: '18px', CSSStyles: { 'font-size': '13px' }, enabled: false // start disabled because no SQLCMD variable values have been edited yet }).component(); loadSqlCmdVarsButton.onDidClick(async () => { for (const key of this.sqlCmdVars!.keys()) { this.sqlCmdVars!.set(key, this.getDefaultSqlCmdValue(key)); } const data = this.convertSqlCmdVarsToTableFormat(this.sqlCmdVars!); await (this.sqlCmdVariablesTable)!.updateProperties({ dataValues: data }); this.updateRevertSqlCmdVarsButtonState(); this.tryEnableGenerateScriptAndPublishButtons(); }); return loadSqlCmdVarsButton; } /** * Gets the default value of a SQLCMD variable for a project * @param varName * @returns value defined in the sqlproj file, or blank string if not defined */ private getDefaultSqlCmdValue(varName: string): string { return this.project.sqlCmdVariables.get(varName) ?? ''; } private createSelectConnectionButton(view: azdataType.ModelView): azdataType.Component { this.selectConnectionButton = view.modelBuilder.button().withProps({ ariaLabel: constants.selectConnection, title: constants.selectConnection, iconPath: IconPathHelper.selectConnection, height: '16px', width: '16px' }).component(); this.selectConnectionButton.onDidClick(async () => { let connection = await utils.getAzdataApi()!.connection.openConnectionDialog(); this.connectionId = connection.connectionId; this.serverName = connection.options['server']; let connectionTextboxValue: string = getConnectionName(connection); await this.updateConnectionComponents(connectionTextboxValue, this.connectionId, connection.options.database); }); return this.selectConnectionButton; } private async updateConnectionComponents(connectionTextboxValue: string, connectionId: string, database: string) { this.targetConnectionTextBox!.value = connectionTextboxValue; await this.targetConnectionTextBox!.updateProperty('title', connectionTextboxValue); if (database && database !== constants.master) { this.targetDatabaseName = database; } // populate database dropdown with the databases for this connection if (connectionId) { const databaseValues = (await utils.getAzdataApi()!.connection.listDatabases(connectionId)) // filter out system dbs .filter(db => !constants.systemDbs.includes(db)); this.targetDatabaseDropDown!.values = databaseValues; // change icon to the one without a plus sign this.selectConnectionButton!.iconPath = IconPathHelper.connect; } } private createSelectProfileButton(view: azdataType.ModelView): azdataType.ButtonComponent { let loadProfileButton: azdataType.ButtonComponent = view.modelBuilder.button().withProps({ label: constants.selectProfile, title: constants.selectProfile, ariaLabel: constants.selectProfile, width: '90px', height: '25px', secondary: true, }).component(); loadProfileButton.onDidClick(async () => { const fileUris = await promptForPublishProfile(this.project.projectFolderPath); if (!fileUris || fileUris.length === 0) { return; } 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.targetDatabaseName = ''; this.connectionId = result.connectionId; this.serverName = result.serverName; await this.updateConnectionComponents(result.connection, this.connectionId, result.databaseName); // set options coming from the publish profiles to deployment options this.setDeploymentOptions(result.options); if ((>result.sqlCmdVariables).size) { // add SQLCMD Variables table if it wasn't there before and the profile had sqlcmd variables if (this.project.sqlCmdVariables.size === 0 && this.sqlCmdVars?.size === 0) { this.formBuilder?.addFormItem(this.sqlCmdVariablesFormComponentGroup); } } else if (this.project.sqlCmdVariables.size === 0) { // remove the table if there are no SQLCMD variables in the project and loaded profile this.formBuilder?.removeFormItem(this.sqlCmdVariablesFormComponentGroup); } for (let key of result.sqlCmdVariables.keys()) { this.sqlCmdVars?.set(key, result.sqlCmdVariables.get(key)!); } this.updateRevertSqlCmdVarsButtonState(); this.deploymentOptions = result.options; const data = this.convertSqlCmdVarsToTableFormat(this.getSqlCmdVariablesForPublish()); await (this.sqlCmdVariablesTable).updateProperties({ dataValues: data }); // show file path in text box and hover text this.loadProfileTextBox!.value = fileUris[0].fsPath; await this.loadProfileTextBox!.updateProperty('title', fileUris[0].fsPath); this.profileUsed = true; this.publishProfileUri = fileUris[0]; } }); return loadProfileButton; } private createSaveProfileAsButton(view: azdataType.ModelView): azdataType.ButtonComponent { let saveProfileAsButton: azdataType.ButtonComponent = view.modelBuilder.button().withProps({ label: constants.saveProfileAsButtonText, title: constants.saveProfileAsButtonText, ariaLabel: constants.saveProfileAsButtonText, width: cssStyles.PublishingOptionsButtonWidth, height: '25px', secondary: true }).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'], } } ); if (!filePath) { return; } if (this.savePublishProfile) { const targetConnectionString = this.connectionId ? await utils.getAzdataApi()!.connection.getConnectionString(this.connectionId, false) : ''; const targetDatabaseName = this.targetDatabaseName ?? ''; const deploymentOptions = await this.getDeploymentOptions(); await this.savePublishProfile(filePath.fsPath, targetDatabaseName, targetConnectionString, this.getSqlCmdVariablesForPublish(), deploymentOptions); TelemetryReporter.sendActionEvent(TelemetryViews.SqlProjectPublishDialog, TelemetryActions.profileSaved); } this.profileUsed = true; this.publishProfileUri = filePath; await this.project.addNoneItem(path.relative(this.project.projectFolderPath, filePath.fsPath)); void vscode.commands.executeCommand(constants.refreshDataWorkspaceCommand); //refresh data workspace to load the newly added profile to the tree }); return saveProfileAsButton; } private convertSqlCmdVarsToTableFormat(sqlCmdVars: Map): azdataType.DeclarativeTableCellValue[][] { let data = []; for (const [key, value] of sqlCmdVars) { data.push([{ value: key }, { value: value! }]); } return data; } /** * Enables or disables "Revert SQLCMD variable values" button depending on whether there are changes * */ private updateRevertSqlCmdVarsButtonState(): void { // no SQLCMD vars -> no button to update state for if (!this.revertSqlCmdVarsButton) { return; } let revertButtonEnabled = false; for (const key of this.sqlCmdVars!.keys()) { if (this.sqlCmdVars!.get(key) !== this.getDefaultSqlCmdValue(key)) { revertButtonEnabled = true; break; } } this.revertSqlCmdVarsButton.enabled = revertButtonEnabled; } // only enable "Generate Script" and "Publish" buttons if all fields are filled private tryEnableGenerateScriptAndPublishButtons(): void { 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 && this.imageTagDropDown!.value && this.eulaCheckBox?.checked) { publishEnabled = true; // only publish is supported for container } this.dialog.okButton.enabled = publishEnabled; this.dialog.customButtons[0].enabled = generateScriptEnabled; } private allSqlCmdVariablesFilled(): boolean { for (let key in this.sqlCmdVars) { if (this.sqlCmdVars.get(key) === '' || this.sqlCmdVars.get(key) === undefined) { return false; } } 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.AdvancedOptionsButton, secondary: true, width: cssStyles.PublishingOptionsButtonWidth }).component(); const optionsRow = view.modelBuilder.flexContainer().withItems([this.optionsButton], { CSSStyles: { flex: '0 0 auto', 'margin': '-8px 0 0 307px' } }).withLayout({ flexFlow: 'row', alignItems: 'center' }).component(); this.toDispose.push(this.optionsButton.onDidClick(async () => { TelemetryReporter.sendActionEvent(TelemetryViews.SqlProjectPublishDialog, TelemetryActions.publishOptionsOpened); // 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 { const defaultDeploymentOptions = await utils.getDefaultPublishDeploymentOptions(this.project) as DeploymentOptions; if (defaultDeploymentOptions && defaultDeploymentOptions.excludeObjectTypes !== undefined) { // For publish dialog no default exclude options should exists defaultDeploymentOptions.excludeObjectTypes.value = []; } return defaultDeploymentOptions; } /* * Sets the default deployment options to deployment options model object */ public setDeploymentOptions(deploymentOptions: DeploymentOptions | undefined): void { this.deploymentOptions = deploymentOptions; } } export function promptForPublishProfile(defaultPath: string): Thenable { return vscode.window.showOpenDialog( { title: constants.selectProfile, canSelectFiles: true, canSelectFolders: false, canSelectMany: false, defaultUri: vscode.Uri.file(defaultPath), filters: { [constants.publishSettingsFiles]: ['publish.xml'] } } ); }