diff --git a/extensions/sql-database-projects/images/dark/edit.svg b/extensions/sql-database-projects/images/dark/edit.svg new file mode 100644 index 0000000000..345362da0a --- /dev/null +++ b/extensions/sql-database-projects/images/dark/edit.svg @@ -0,0 +1,3 @@ + + + diff --git a/extensions/sql-database-projects/images/dark/folder.svg b/extensions/sql-database-projects/images/dark/folder.svg new file mode 100644 index 0000000000..64cbba1769 --- /dev/null +++ b/extensions/sql-database-projects/images/dark/folder.svg @@ -0,0 +1,3 @@ + + + diff --git a/extensions/sql-database-projects/images/light/edit.svg b/extensions/sql-database-projects/images/light/edit.svg new file mode 100644 index 0000000000..345362da0a --- /dev/null +++ b/extensions/sql-database-projects/images/light/edit.svg @@ -0,0 +1,3 @@ + + + diff --git a/extensions/sql-database-projects/images/light/folder.svg b/extensions/sql-database-projects/images/light/folder.svg new file mode 100644 index 0000000000..64cbba1769 --- /dev/null +++ b/extensions/sql-database-projects/images/light/folder.svg @@ -0,0 +1,3 @@ + + + diff --git a/extensions/sql-database-projects/src/common/constants.ts b/extensions/sql-database-projects/src/common/constants.ts index 1a36a03dda..057fcf0af6 100644 --- a/extensions/sql-database-projects/src/common/constants.ts +++ b/extensions/sql-database-projects/src/common/constants.ts @@ -63,26 +63,26 @@ export function deleteConfirmationContents(toDelete: string) { return localize(' // Publish dialog strings -export const publishDialogName = localize('publishDialogName', "Publish Database"); +export const publishDialogName = localize('publishDialogName', "Publish project"); export const publishDialogOkButtonText = localize('publishDialogOkButtonText', "Publish"); export const cancelButtonText = localize('cancelButtonText', "Cancel"); export const generateScriptButtonText = localize('generateScriptButtonText', "Generate Script"); -export const targetDatabaseSettings = localize('targetDatabaseSettings', "Target Database Settings"); export const databaseNameLabel = localize('databaseNameLabel', "Database"); -export const targetConnectionLabel = localize('targetConnectionLabel', "Target Connection"); -export const editConnectionButtonText = localize('editConnectionButtonText', "Edit"); -export const clearButtonText = localize('clearButtonText', "Clear"); +export const targetConnectionLabel = localize('targetConnectionLabel', "Connection"); export const dataSourceRadioButtonLabel = localize('dataSourceRadioButtonLabel', "Data sources"); export const connectionRadioButtonLabel = localize('connectionRadioButtonLabel', "Connections"); export const selectConnectionRadioButtonsTitle = localize('selectconnectionRadioButtonsTitle', "Specify connection from:"); export const dataSourceDropdownTitle = localize('dataSourceDropdownTitle', "Data source"); export const noDataSourcesText = localize('noDataSourcesText', "No data sources in this project"); -export const loadProfileButtonText = localize('loadProfileButtonText', "Load Profile..."); +export const loadProfilePlaceholderText = localize('loadProfilePlaceholderText', "Load profile..."); export const profileReadError = localize('profileReadError', "Could not load the profile file."); export const sqlCmdTableLabel = localize('sqlCmdTableLabel', "SQLCMD Variables"); export const sqlCmdVariableColumn = localize('sqlCmdVariableColumn', "Name"); export const sqlCmdValueColumn = localize('sqlCmdValueColumn', "Value"); export const loadSqlCmdVarsButtonTitle = localize('reloadValuesFromProjectButtonTitle', "Reload values from project"); +export const profile = localize('profile', "Profile"); +export const selectConnection = localize('selectConnection', "Select connection"); +export const connection = localize('connection', "Connection"); // Error messages @@ -222,3 +222,6 @@ export enum DatabaseProjectItemType { dataSourceRoot = 'databaseProject.itemType.dataSourceRoot', dataSource = 'databaseProject.itemType.dataSource' } + +// System dbs +export const systemDbs = ['master', 'msdb', 'tempdb', 'model']; diff --git a/extensions/sql-database-projects/src/common/iconHelper.ts b/extensions/sql-database-projects/src/common/iconHelper.ts index 4034daf158..f66ee85482 100644 --- a/extensions/sql-database-projects/src/common/iconHelper.ts +++ b/extensions/sql-database-projects/src/common/iconHelper.ts @@ -21,6 +21,8 @@ export class IconPathHelper { public static referenceDatabase: IconPath; public static refresh: IconPath; + public static folder: IconPath; + public static edit: IconPath; public static setExtensionContext(extensionContext: vscode.ExtensionContext) { IconPathHelper.extensionContext = extensionContext; @@ -34,6 +36,8 @@ export class IconPathHelper { IconPathHelper.referenceDatabase = IconPathHelper.makeIcon('reference-database'); IconPathHelper.refresh = IconPathHelper.makeIcon('refresh'); + IconPathHelper.folder = IconPathHelper.makeIcon('folder'); + IconPathHelper.edit = IconPathHelper.makeIcon('edit'); } private static makeIcon(name: string) { diff --git a/extensions/sql-database-projects/src/common/uiConstants.ts b/extensions/sql-database-projects/src/common/uiConstants.ts index fb41ad494b..3e113ca4d1 100644 --- a/extensions/sql-database-projects/src/common/uiConstants.ts +++ b/extensions/sql-database-projects/src/common/uiConstants.ts @@ -9,4 +9,6 @@ export namespace cssStyles { export const tableHeader = { ...text, 'text-align': 'left', 'border': 'none', 'font-size': '12px', 'font-weight': 'normal', 'color': '#666666' }; export const tableRow = { ...text, 'border-top': 'solid 1px #ccc', 'border-bottom': 'solid 1px #ccc', 'border-left': 'none', 'border-right': 'none', 'font-size': '12px' }; export const titleFontSize = 13; + export const publishDialogLabelWidth = '205px'; + export const publishDialogTextboxWidth = '190px'; } diff --git a/extensions/sql-database-projects/src/dialogs/publishDatabaseDialog.ts b/extensions/sql-database-projects/src/dialogs/publishDatabaseDialog.ts index dd7daa50fc..8bd01b0d2f 100644 --- a/extensions/sql-database-projects/src/dialogs/publishDatabaseDialog.ts +++ b/extensions/sql-database-projects/src/dialogs/publishDatabaseDialog.ts @@ -24,16 +24,15 @@ export class PublishDatabaseDialog { public dialog: azdata.window.Dialog; public publishTab: azdata.window.DialogTab; private targetConnectionTextBox: azdata.InputBoxComponent | undefined; - private targetConnectionFormComponent: azdata.FormComponent | undefined; private dataSourcesFormComponent: azdata.FormComponent | undefined; private dataSourcesDropDown: azdata.DropDownComponent | undefined; - private targetDatabaseTextBox: azdata.InputBoxComponent | undefined; + private targetDatabaseDropDown: azdata.DropDownComponent | undefined; private connectionsRadioButton: azdata.RadioButtonComponent | undefined; private dataSourcesRadioButton: azdata.RadioButtonComponent | undefined; - private loadProfileButton: azdata.ButtonComponent | undefined; private sqlCmdVariablesTable: azdata.DeclarativeTableComponent | undefined; private sqlCmdVariablesFormComponentGroup: azdata.FormComponentGroup | undefined; private loadSqlCmdVarsButton: azdata.ButtonComponent | undefined; + private loadProfileTextBox: azdata.InputBoxComponent | undefined; private formBuilder: azdata.FormBuilder | undefined; private connectionId: string | undefined; @@ -84,20 +83,9 @@ export class PublishDatabaseDialog { // TODO : enable using this when data source creation is enabled this.createRadioButtons(view); - this.targetConnectionFormComponent = this.createTargetConnectionComponent(view); - - this.targetDatabaseTextBox = view.modelBuilder.inputBox().withProperties({ - value: this.getDefaultDatabaseName(), - ariaLabel: constants.databaseNameLabel - }).component(); this.dataSourcesFormComponent = this.createDataSourcesFormComponent(view); - this.targetDatabaseTextBox.onTextChanged(() => { - this.tryEnableGenerateScriptAndOkButtons(); - }); - - this.loadProfileButton = this.createLoadProfileButton(view); this.sqlCmdVariablesTable = this.createSqlCmdTable(view); this.loadSqlCmdVarsButton = this.createLoadSqlCmdVarsButton(view); @@ -115,29 +103,33 @@ export class PublishDatabaseDialog { title: constants.sqlCmdTableLabel }; + const profileRow = this.createProfileRow(view); + const connectionRow = this.createConnectionRow(view); + const databaseRow = this.createDatabaseRow(view); + + const horizontalFormSection = view.modelBuilder.flexContainer().withLayout({ flexFlow: 'column' }).component(); + horizontalFormSection.addItems([profileRow, connectionRow, databaseRow]); + + this.formBuilder = view.modelBuilder.formContainer() .withFormItems([ { - title: constants.targetDatabaseSettings, + title: '', components: [ { - title: '', - component: this.loadProfileButton + component: horizontalFormSection, + title: '' }, /* TODO : enable using this when data source creation is enabled { title: constants.selectConnectionRadioButtonsTitle, component: selectConnectionRadioButtons },*/ - this.targetConnectionFormComponent, - { - title: constants.databaseNameLabel, - component: this.targetDatabaseTextBox - } ] } ], { - horizontal: false + horizontal: false, + titleFontSize: cssStyles.titleFontSize }) .withLayout({ width: '100%' @@ -145,7 +137,7 @@ export class PublishDatabaseDialog { // add SQLCMD variables table if the project has any if (Object.keys(this.project.sqlCmdVariables).length > 0) { - this.formBuilder.addFormItem(this.sqlCmdVariablesFormComponentGroup, { titleFontSize: cssStyles.titleFontSize }); + this.formBuilder.addFormItem(this.sqlCmdVariablesFormComponentGroup); } let formModel = this.formBuilder.component(); @@ -225,7 +217,7 @@ export class PublishDatabaseDialog { } public getTargetDatabaseName(): string { - return this.targetDatabaseTextBox?.value ?? ''; + return this.targetDatabaseDropDown?.value ?? ''; } public getDefaultDatabaseName(): string { @@ -242,9 +234,10 @@ export class PublishDatabaseDialog { this.connectionsRadioButton.checked = true; this.connectionsRadioButton.onDidClick(() => { this.formBuilder!.removeFormItem(this.dataSourcesFormComponent); - this.formBuilder!.insertFormItem(this.targetConnectionFormComponent, 2); + // TODO: fix this when data sources are enabled again + // this.formBuilder!.insertFormItem(this.targetConnectionTextBox, 2); this.connectionIsDataSource = false; - this.targetDatabaseTextBox!.value = this.getDefaultDatabaseName(); + this.targetDatabaseDropDown!.value = this.getDefaultDatabaseName(); }); this.dataSourcesRadioButton = view.modelBuilder.radioButton() @@ -254,7 +247,8 @@ export class PublishDatabaseDialog { }).component(); this.dataSourcesRadioButton.onDidClick(() => { - this.formBuilder!.removeFormItem(this.targetConnectionFormComponent); + // TODO: fix this when data sources are enabled again + // this.formBuilder!.removeFormItem(this.targetConnectionTextBox); this.formBuilder!.insertFormItem(this.dataSourcesFormComponent, 2); this.connectionIsDataSource = true; @@ -270,25 +264,19 @@ export class PublishDatabaseDialog { return flexRadioButtonsModel; } - private createTargetConnectionComponent(view: azdata.ModelView): azdata.FormComponent { + private createTargetConnectionComponent(view: azdata.ModelView): azdata.InputBoxComponent { this.targetConnectionTextBox = view.modelBuilder.inputBox().withProperties({ value: '', ariaLabel: constants.targetConnectionLabel, - enabled: false + placeHolder: constants.selectConnection, + width: cssStyles.publishDialogTextboxWidth }).component(); this.targetConnectionTextBox.onTextChanged(() => { this.tryEnableGenerateScriptAndOkButtons(); }); - let editConnectionButton: azdata.Component = this.createEditConnectionButton(view); - let clearButton: azdata.Component = this.createClearButton(view); - - return { - title: constants.targetConnectionLabel, - component: this.targetConnectionTextBox, - actions: [editConnectionButton, clearButton] - }; + return this.targetConnectionTextBox; } private createDataSourcesFormComponent(view: azdata.ModelView): azdata.FormComponent { @@ -335,10 +323,70 @@ export class PublishDatabaseDialog { private setDatabaseToSelectedDataSourceDatabase(): void { if ((this.dataSourcesDropDown!.value)?.database) { - this.targetDatabaseTextBox!.value = (this.dataSourcesDropDown!.value).database; + this.targetDatabaseDropDown!.value = (this.dataSourcesDropDown!.value).database; } } + private createProfileRow(view: azdata.ModelView): azdata.FlexContainer { + const loadProfileButton = this.createLoadProfileButton(view); + this.loadProfileTextBox = view.modelBuilder.inputBox().withProperties({ + placeHolder: constants.loadProfilePlaceholderText, + ariaLabel: constants.profile, + width: cssStyles.publishDialogTextboxWidth + }).component(); + + const profileLabel = view.modelBuilder.text().withProperties({ + value: constants.profile, + width: cssStyles.publishDialogLabelWidth + }).component(); + + const profileRow = view.modelBuilder.flexContainer().withItems([profileLabel, this.loadProfileTextBox], { flex: '0 0 auto', CSSStyles: { 'margin-right': '10px' } }).withLayout({ flexFlow: 'row', alignItems: 'center' }).component(); + profileRow.insertItem(loadProfileButton, 2, { CSSStyles: { 'margin-right': '0px' } }); + + return profileRow; + } + + private createConnectionRow(view: azdata.ModelView): azdata.FlexContainer { + this.targetConnectionTextBox = this.createTargetConnectionComponent(view); + const selectConnectionButton: azdata.Component = this.createSelectConnectionButton(view); + + const connectionLabel = view.modelBuilder.text().withProperties({ + value: constants.connection, + requiredIndicator: true, + width: cssStyles.publishDialogLabelWidth + }).component(); + + const connectionRow = view.modelBuilder.flexContainer().withItems([connectionLabel, this.targetConnectionTextBox], { flex: '0 0 auto', CSSStyles: { 'margin-right': '10px' } }).withLayout({ flexFlow: 'row', alignItems: 'center' }).component(); + connectionRow.insertItem(selectConnectionButton, 2, { CSSStyles: { 'margin-right': '0px' } }); + + return connectionRow; + } + + private createDatabaseRow(view: azdata.ModelView): azdata.FlexContainer { + this.targetDatabaseDropDown = view.modelBuilder.dropDown().withProperties({ + value: this.getDefaultDatabaseName(), + ariaLabel: constants.databaseNameLabel, + required: true, + width: cssStyles.publishDialogTextboxWidth, + editable: true, + fireOnTextChange: true + }).component(); + + this.targetDatabaseDropDown.onValueChanged(() => { + this.tryEnableGenerateScriptAndOkButtons(); + }); + + const databaseLabel = view.modelBuilder.text().withProperties({ + 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; + } + private createSqlCmdTable(view: azdata.ModelView): azdata.DeclarativeTableComponent { this.sqlCmdVars = { ...this.project.sqlCmdVariables }; @@ -362,7 +410,7 @@ export class PublishDatabaseDialog { headerCssStyles: cssStyles.tableHeader, rowCssStyles: cssStyles.tableRow }], - width: '100%' + width: '410px' }).component(); table.onDataChanged(() => { @@ -402,14 +450,15 @@ export class PublishDatabaseDialog { return loadSqlCmdVarsButton; } - private createEditConnectionButton(view: azdata.ModelView): azdata.Component { - let editConnectionButton: azdata.ButtonComponent = view.modelBuilder.button().withProperties({ - label: constants.editConnectionButtonText, - title: constants.editConnectionButtonText, - ariaLabel: constants.editConnectionButtonText + private createSelectConnectionButton(view: azdata.ModelView): azdata.Component { + let selectConnectionButton: azdata.ButtonComponent = view.modelBuilder.button().withProperties({ + ariaLabel: constants.selectConnection, + iconPath: IconPathHelper.edit, + height: '16px', + width: '16px' }).component(); - editConnectionButton.onDidClick(async () => { + selectConnectionButton.onDidClick(async () => { let connection = await azdata.connection.openConnectionDialog(); this.connectionId = connection.connectionId; @@ -420,35 +469,28 @@ export class PublishDatabaseDialog { this.targetConnectionTextBox!.value = await azdata.connection.getConnectionString(connection.connectionId, false); } + // populate database dropdown with the databases for this connection + const databaseValues = (await azdata.connection.listDatabases(this.connectionId)) + // filter out system dbs + .filter(db => constants.systemDbs.find(systemdb => db === systemdb) === undefined); + + this.targetDatabaseDropDown!.values = databaseValues; + // change the database inputbox value to the connection's database if there is one if (connection.options.database && connection.options.database !== constants.master) { - this.targetDatabaseTextBox!.value = connection.options.database; + this.targetDatabaseDropDown!.value = connection.options.database; } }); - return editConnectionButton; - } - - private createClearButton(view: azdata.ModelView): azdata.Component { - let clearButton: azdata.ButtonComponent = view.modelBuilder.button().withProperties({ - label: constants.clearButtonText, - title: constants.clearButtonText, - ariaLabel: constants.clearButtonText - }).component(); - - clearButton.onDidClick(() => { - this.targetConnectionTextBox!.value = ''; - }); - - return clearButton; + return selectConnectionButton; } private createLoadProfileButton(view: azdata.ModelView): azdata.ButtonComponent { let loadProfileButton: azdata.ButtonComponent = view.modelBuilder.button().withProperties({ - label: constants.loadProfileButtonText, - title: constants.loadProfileButtonText, - ariaLabel: constants.loadProfileButtonText, - width: '120px' + ariaLabel: constants.loadProfilePlaceholderText, + iconPath: IconPathHelper.folder, + height: '16px', + width: '15px' }).component(); loadProfileButton.onDidClick(async () => { @@ -470,7 +512,7 @@ export class PublishDatabaseDialog { if (this.readPublishProfile) { const result = await this.readPublishProfile(fileUris[0]); - (this.targetDatabaseTextBox).value = result.databaseName; + (this.targetDatabaseDropDown).value = result.databaseName; this.connectionId = result.connectionId; (this.targetConnectionTextBox).value = result.connectionString; @@ -489,12 +531,15 @@ export class PublishDatabaseDialog { if (Object.keys(result.sqlCmdVariables).length) { // add SQLCMD Variables table if it wasn't there before if (Object.keys(this.project.sqlCmdVariables).length === 0) { - this.formBuilder?.addFormItem(this.sqlCmdVariablesFormComponentGroup, { titleFontSize: cssStyles.titleFontSize }); + this.formBuilder?.addFormItem(this.sqlCmdVariablesFormComponentGroup); } } else if (Object.keys(this.project.sqlCmdVariables).length === 0) { // remove the table if there are no SQLCMD variables in the project and loaded profile this.formBuilder?.removeFormItem(this.sqlCmdVariablesFormComponentGroup); } + + // show file path in text box + this.loadProfileTextBox!.value = fileUris[0].fsPath; } }); @@ -512,8 +557,8 @@ export class PublishDatabaseDialog { // only enable Generate Script and Ok buttons if all fields are filled private tryEnableGenerateScriptAndOkButtons(): void { - if ((this.targetConnectionTextBox!.value && this.targetDatabaseTextBox!.value - || this.connectionIsDataSource && this.targetDatabaseTextBox!.value) + 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;