diff --git a/extensions/mssql/src/objectManagement/constants.ts b/extensions/mssql/src/objectManagement/constants.ts index 5bc772b46e..159a8f07fb 100644 --- a/extensions/mssql/src/objectManagement/constants.ts +++ b/extensions/mssql/src/objectManagement/constants.ts @@ -36,6 +36,7 @@ export const DatabaseGeneralPropertiesDocUrl = 'https://learn.microsoft.com/sql/ export const DatabaseOptionsPropertiesDocUrl = 'https://learn.microsoft.com/sql/relational-databases/databases/database-properties-options-page' export const DropDatabaseDocUrl = 'https://learn.microsoft.com/sql/t-sql/statements/drop-database-transact-sql'; export const DatabaseScopedConfigurationPropertiesDocUrl = 'https://learn.microsoft.com/sql/t-sql/statements/alter-database-scoped-configuration-transact-sql' +export const DatabaseFilesPropertiesDocUrl = 'https://learn.microsoft.com/sql/relational-databases/databases/database-properties-files-page' export const enum TelemetryActions { CreateObject = 'CreateObject', diff --git a/extensions/mssql/src/objectManagement/interfaces.ts b/extensions/mssql/src/objectManagement/interfaces.ts index 2a103ea952..bae4b5e08b 100644 --- a/extensions/mssql/src/objectManagement/interfaces.ts +++ b/extensions/mssql/src/objectManagement/interfaces.ts @@ -455,6 +455,8 @@ export interface Database extends ObjectManagement.SqlObject { encryptionEnabled: boolean; restrictAccess?: string; databaseScopedConfigurations: DatabaseScopedConfigurationsInfo[]; + isFilesTabSupported?: boolean; + files?: DatabaseFile[]; } export interface DatabaseViewInfo extends ObjectManagement.ObjectViewInfo { @@ -466,7 +468,6 @@ export interface DatabaseViewInfo extends ObjectManagement.ObjectViewInfo { } +export const enum FileGrowthType { + KB = 0, + Percent = 1, + None = 99 +} + export interface DatabaseFile { + id: number; name: string; type: string; path: string; fileGroup: string; + fileNameWithExtension: string; + sizeInMb: number; + isAutoGrowthEnabled: boolean; + autoFileGrowth: number; + autoFileGrowthType: FileGrowthType; + maxSizeLimitInMb: number } diff --git a/extensions/mssql/src/objectManagement/localizedConstants.ts b/extensions/mssql/src/objectManagement/localizedConstants.ts index 56eed12842..3e4dcc012f 100644 --- a/extensions/mssql/src/objectManagement/localizedConstants.ts +++ b/extensions/mssql/src/objectManagement/localizedConstants.ts @@ -155,6 +155,7 @@ export const NameText = localize('objectManagement.nameLabel', "Name"); export const GeneralSectionHeader = localize('objectManagement.generalSectionHeader', "General"); export const AdvancedSectionHeader = localize('objectManagement.advancedSectionHeader', "Advanced"); export const OptionsSectionHeader = localize('objectManagement.optionsSectionHeader', "Options"); +export const FilesSectionHeader = localize('objectManagement.optionsSectionHeader', "Files"); export const PasswordText = localize('objectManagement.passwordLabel', "Password"); export const ConfirmPasswordText = localize('objectManagement.confirmPasswordLabel', "Confirm password"); export const EnabledText = localize('objectManagement.enabledLabel', "Enabled"); @@ -323,6 +324,46 @@ export const DatabaseScopedOptionsColumnHeader = localize('objectManagement.data export const ValueForPrimaryColumnHeader = localize('objectManagement.databaseProperties.valueForPrimaryColumnHeader', "Value for Primary"); export const ValueForSecondaryColumnHeader = localize('objectManagement.databaseProperties.valueForSecondaryColumnHeader', "Value for Secondary"); export const SetSecondaryText = localize('objectManagement.databaseProperties.setSecondaryText', "Set Secondary same as Primary"); +export const DatabaseNameText = localize('objectManagement.databaseProperties.databaseNameLabel', "Database Name"); +export const UseFullTextIndexingText = localize('objectManagement.databaseProperties.useFullTextIndexingText', "Use full-text indexing"); +export const LogicalNameText = localize('objectManagement.databaseProperties.logicalNameText', "Logical Name"); +export const FileTypeText = localize('objectManagement.databaseProperties.fileTypeText', "File Type"); +export const FilegroupText = localize('objectManagement.databaseProperties.filegroupText', "Filegroup"); +export const AutogrowthMaxsizeText = localize('objectManagement.databaseProperties.autogrowthMaxsizeText', "Autogrowth / Maxsize"); +export const PathText = localize('objectManagement.databaseProperties.pathText', "Path"); +export const FileNameText = localize('objectManagement.databaseProperties.fileNameText', "File Name"); +export const DatabaseFilesText = localize('objectManagement.databaseProperties.databaseFilesText', "Database files"); +export const AddDatabaseFilesText = localize('objectManagement.databaseProperties.addDatabaseFilesText', "Add Database file"); +export const EditDatabaseFilesText = (fileName: string) => localize('objectManagement.databaseProperties.editDatabaseFilesText', "Edit Database file - {0}", fileName); +export const AddButton = localize('objectManagement.databaseProperties.addButton', "Add"); +export const EditButton = localize('objectManagement.databaseProperties.editButton', "Edit"); +export const RemoveButton = localize('objectManagement.databaseProperties.removeButton', "Remove"); +export const SizeInMbText = localize('objectManagement.databaseProperties.size', "Size (MB)"); +export const EnableAutogrowthText = localize('objectManagement.databaseProperties.enableAutogrowthText', "Enable Autogrowth"); +export const FileGrowthText = localize('objectManagement.databaseProperties.fileGrowthText', "File Growth"); +export const MaximumFileSizeText = localize('objectManagement.databaseProperties.maximumFileSizeText', "Maximum File Size"); +export const InPercentAutogrowthText = localize('objectManagement.databaseProperties.inPercentAutogrowthText', "In Percent"); +export const InMegabytesAutogrowthText = localize('objectManagement.databaseProperties.inMegabytesAutogrowthText', "In Megabytes"); +export const LimitedToMBFileSizeText = localize('objectManagement.databaseProperties.limitedToMBFileSizeText', "Limited to (MB)"); +export const UnlimitedFileSizeText = localize('objectManagement.databaseProperties.unlimitedFileSizeText', "Unlimited"); +export const NoneText = localize('objectManagement.databaseProperties.noneText', "None"); +export function AutoGrowthValueStringGenerator(isFileGrowthSupported: boolean, fileGrowth: string, isFleGrowthInPercent: boolean, maxFileSize: number): string { + const maxSizelimitation = maxFileSize === -1 + ? localize('objectManagement.databaseProperties.autoGrowthValueConversion.unlimited', "Unlimited") + : localize('objectManagement.databaseProperties.autoGrowthValueConversion.limitation', "Limited to {0} MB", maxFileSize); + return isFileGrowthSupported ? localize('objectManagement.databaseProperties.autoGrowthValueConversion', "By {0} {1}, {2}", fileGrowth, isFleGrowthInPercent ? "Percent" : "MB", maxSizelimitation) + : localize('objectManagement.databaseProperties.autoGrowthValueConversion', "{0}", maxSizelimitation); +} +export const FileGroupForLogTypeText = localize('objectManagement.databaseProperties.fileGroupNotApplicableText', "Not Applicable"); +export const FileGroupForFilestreamTypeText = localize('objectManagement.databaseProperties.fileGroupNotApplicableText', "No Applicable Filegroup"); +export const DuplicateLogicalNameError = (name: string) => localize('objectManagement.databaseProperties.fileGroupNotApplicableText', "DataFile '{0}' could not be added to the collection, because it already exists.", name); +export const FileNameExistsError = (name: string) => localize('objectManagement.databaseProperties.fileNameExistsError', "The Logical file name '{0}' is already in use. Choose a different name.", name); +export const FileAlreadyExistsError = (fullFilePath: string) => localize('objectManagement.databaseProperties.fileNameExistsError', "Cannot create file '{0}' because it already exists.", fullFilePath); +export const FileSizeLimitError = localize('objectManagement.databaseProperties.fileSizeLimitError', "Maximum file size cannot be less than size"); +export const FilegrowthLimitError = localize('objectManagement.databaseProperties.filegrowthLimitError', "Filegrowth cannot be greater than the Maximum file size for a file"); +export const RowsDataFileType = localize('objectManagement.databaseProperties.rowsDataFileType', "ROWS Data"); +export const LogFiletype = localize('objectManagement.databaseProperties.logfiletype', "LOG"); +export const FilestreamFileType = localize('objectManagement.databaseProperties.filestreamFileType', "FILESTREAM Data"); // Util functions export function getNodeTypeDisplayName(type: string, inTitle: boolean = false): string { diff --git a/extensions/mssql/src/objectManagement/objectManagementService.ts b/extensions/mssql/src/objectManagement/objectManagementService.ts index 6956fc9b4c..468ef702b8 100644 --- a/extensions/mssql/src/objectManagement/objectManagementService.ts +++ b/extensions/mssql/src/objectManagement/objectManagementService.ts @@ -470,12 +470,16 @@ export class TestObjectManagementService implements IObjectManagementService { collationNames: { defaultValueIndex: 0, options: ['Latin1_General_100_CI_AS_KS_WS', 'Latin1_General_100_CI_AS_KS_WS_SC'] }, compatibilityLevels: { defaultValueIndex: 0, options: ['SQL Server 2008', 'SQL Server 2012', 'SQL Server 2014', 'SQL Server 2016', 'SQL Server 2017', 'SQL Server 2019'] }, containmentTypes: { defaultValueIndex: 0, options: ['NONE', 'PARTIAL'] }, + loginNames: { defaultValueIndex: 0, options: ['user1', 'user2', 'user3'] }, restrictAccessOptions: ['MULTI_USER', 'RESTRICTED_USER', 'SINGLE_USER'], recoveryModels: { defaultValueIndex: 0, options: ['FULL', 'SIMPLE', 'BULK_LOGGED'] }, pageVerifyOptions: ['CHECKSUM', 'NONE', 'TORN_PAGE_DETECTION'], dscElevateOptions: ['OFF', 'WHEN_SUPPORTED', 'FAIL_UNSUPPORTED'], dscEnableDisableOptions: ['ENABLED', 'DISABLED'], dscOnOffOptions: ['ON', 'OFF'], + rowDataFileGroupsOptions: ['PRIMARY', 'RowDataGroup1', 'RowDataGroup2'], + fileStreamFileGroupsOptions: ['PRIMARY', 'FileStreamGroup1', 'FileStreamGroup2'], + fileTypesOptions: ['ROWS', 'LOG', 'FILESTREAM'], objectInfo: { name: 'Database Properties1', collationName: 'Latin1_General_100_CI_AS_KS_WS', @@ -510,6 +514,11 @@ export class TestObjectManagementService implements IObjectManagementService { { name: 'batch_mode_memory_grant_feedback', valueForPrimary: 'OFF', valueForSecondary: 'OFF' }, { name: 'batch_mode_adaptive_joins', valueForPrimary: 'OFF', valueForSecondary: 'ON' }, { name: 'tsql_scalar_udf_inlining', valueForPrimary: 'ON', valueForSecondary: 'ON' } + ], + isFilesTabSupported: true, + files: [ + { id: 1, name: 'databasefile1', type: 'ROWS Data', path: 'C:\\Temp\\', fileGroup: 'PRIMARY', fileNameWithExtension: 'databasefile1.mdf', sizeInMb: 62, isAutoGrowthEnabled: true, autoFileGrowth: 64, autoFileGrowthType: 0, maxSizeLimitInMb: -1 }, + { id: 2, name: 'databasefile1_Log', type: 'Log', path: 'C:\\Temp\\', fileGroup: 'Not Applicable', fileNameWithExtension: 'databasefile1_log.ldf', sizeInMb: 62, isAutoGrowthEnabled: true, autoFileGrowth: 64, autoFileGrowthType: 1, maxSizeLimitInMb: -1 }, ] } } diff --git a/extensions/mssql/src/objectManagement/ui/databaseDialog.ts b/extensions/mssql/src/objectManagement/ui/databaseDialog.ts index bdaee2c7bf..bd0a58689d 100644 --- a/extensions/mssql/src/objectManagement/ui/databaseDialog.ts +++ b/extensions/mssql/src/objectManagement/ui/databaseDialog.ts @@ -5,14 +5,15 @@ import * as azdata from 'azdata'; import { ObjectManagementDialogBase, ObjectManagementDialogOptions } from './objectManagementDialogBase'; -import { DefaultInputWidth, DefaultTableWidth, getTableHeight } from '../../ui/dialogBase'; +import { DefaultInputWidth, DefaultTableWidth, DefaultMinTableRowCount, DefaultMaxTableRowCount, getTableHeight, DialogButton } from '../../ui/dialogBase'; import { IObjectManagementService } from 'mssql'; import * as localizedConstants from '../localizedConstants'; -import { CreateDatabaseDocUrl, DatabaseGeneralPropertiesDocUrl, DatabaseOptionsPropertiesDocUrl, DatabaseScopedConfigurationPropertiesDocUrl } from '../constants'; -import { Database, DatabaseScopedConfigurationsInfo, DatabaseViewInfo } from '../interfaces'; +import { CreateDatabaseDocUrl, DatabaseGeneralPropertiesDocUrl, DatabaseFilesPropertiesDocUrl, DatabaseOptionsPropertiesDocUrl, DatabaseScopedConfigurationPropertiesDocUrl } from '../constants'; +import { Database, DatabaseFile, DatabaseScopedConfigurationsInfo, DatabaseViewInfo, FileGrowthType } from '../interfaces'; import { convertNumToTwoDecimalStringInMB } from '../utils'; import { isUndefinedOrNull } from '../../types'; import { deepClone } from '../../util/objects'; +import { DatabaseFileDialog } from './databaseFileDialog'; const MAXDOP_Max_Limit = 32767; const PAUSED_RESUMABLE_INDEX_Max_Limit = 71582; @@ -21,6 +22,7 @@ const DscTableRowLength = 15; export class DatabaseDialog extends ObjectManagementDialogBase { // Database Properties tabs private generalTab: azdata.Tab; + private filesTab: azdata.Tab; private optionsTab: azdata.Tab; private dscTab: azdata.Tab; private optionsTabSectionsContainer: azdata.Component[] = []; @@ -43,6 +45,9 @@ export class DatabaseDialog extends ObjectManagementDialogBase { }, { + ariaLabel: localizedConstants.DatabaseNameText, + inputType: 'text', + enabled: this.options.isNewObject, + value: this.objectInfo.name + }); + containers.push(this.createLabelInputContainer(localizedConstants.DatabaseNameText, this.nameInput)); + + // Owner + let loginNames = this.viewInfo.loginNames?.options; + + if (loginNames?.length > 0) { + // Removing login name from the list and adding current owner if not exists + if (!this.viewInfo.loginNames?.options.find(owner => owner === this.objectInfo.owner)) { + loginNames[0] = this.objectInfo.owner; + } else { + loginNames.splice(0, 1); + } + let ownerDropbox = this.createDropdown(localizedConstants.OwnerText, async () => { + this.objectInfo.owner = ownerDropbox.value as string; + }, loginNames, this.objectInfo.owner); + containers.push(this.createLabelInputContainer(localizedConstants.OwnerText, ownerDropbox)); + } + return this.createGroup('', containers, false); + } + + private initializeDatabaseFilesSection(): azdata.GroupContainer { + this.databaseFilesTable = this.modelView.modelBuilder.table().withProps({ + columns: [{ + type: azdata.ColumnType.text, + value: localizedConstants.LogicalNameText + }, { + type: azdata.ColumnType.text, + value: localizedConstants.FileTypeText + }, { + type: azdata.ColumnType.text, + value: localizedConstants.FilegroupText + }, { + type: azdata.ColumnType.text, + value: localizedConstants.SizeInMbText + }, { + type: azdata.ColumnType.text, + value: localizedConstants.AutogrowthMaxsizeText + }, { + type: azdata.ColumnType.text, + value: localizedConstants.PathText + }, { + type: azdata.ColumnType.text, + value: localizedConstants.FileNameText + }], + data: this.objectInfo.files?.map(file => { + return this.convertToDataView(file); + + }), + height: getTableHeight(this.objectInfo.files?.length, DefaultMinTableRowCount, DefaultMaxTableRowCount), + width: DefaultTableWidth, + forceFitColumns: azdata.ColumnSizingMode.DataFit, + CSSStyles: { + 'margin-left': '10px' + } + }).component(); + const addButtonComponent: DialogButton = { + buttonAriaLabel: localizedConstants.AddButton, + buttonHandler: (button) => this.onAddDatabaseFilesButtonClicked(button) + }; + const removeButtonComponent: DialogButton = { + buttonAriaLabel: localizedConstants.RemoveButton, + buttonHandler: () => this.onRemoveDatabaseFilesButtonClicked() + }; + const editbuttonComponent: DialogButton = { + buttonAriaLabel: localizedConstants.EditButton, + buttonHandler: (button) => this.onEditDatabaseFilesButtonClicked(button) + }; + const databaseFilesButtonContainer = this.addButtonsForTable(this.databaseFilesTable, addButtonComponent, removeButtonComponent, editbuttonComponent); + + return this.createGroup(localizedConstants.DatabaseFilesText, [this.databaseFilesTable, databaseFilesButtonContainer], true); + } + + /** + * Converts the file object to a data view object + * @param file database file object + * @returns data view object + */ + private convertToDataView(file: DatabaseFile): any[] { + return [ + file.name, + file.type, + file.fileGroup, + file.sizeInMb, + file.isAutoGrowthEnabled ? localizedConstants.AutoGrowthValueStringGenerator(file.type !== localizedConstants.FilestreamFileType + , file.autoFileGrowth.toString() + , file.autoFileGrowthType === FileGrowthType.Percent + , file.maxSizeLimitInMb) : localizedConstants.NoneText, + file.path, + file.fileNameWithExtension + ]; + } + + private async onAddDatabaseFilesButtonClicked(button: azdata.ButtonComponent): Promise { + // Open file dialog to create file + const result = await this.openDatabaseFileDialog(button); + if (!isUndefinedOrNull(result)) { + this.objectInfo.files?.push(result); + var newData = this.objectInfo.files?.map(file => { + return this.convertToDataView(file); + }); + await this.setTableData(this.databaseFilesTable, newData, DefaultMaxTableRowCount) + } + } + + private async onEditDatabaseFilesButtonClicked(button: azdata.ButtonComponent): Promise { + if (this.databaseFilesTable.selectedRows.length === 1) { + const result = await this.openDatabaseFileDialog(button); + if (!isUndefinedOrNull(result)) { + this.objectInfo.files[this.databaseFilesTable.selectedRows[0]] = result; + var newData = this.objectInfo.files?.map(file => { + return this.convertToDataView(file); + }); + await this.setTableData(this.databaseFilesTable, newData, DefaultMaxTableRowCount) + } + } + } + + /** + * Removes the selected database file from the table + */ + private async onRemoveDatabaseFilesButtonClicked(): Promise { + if (this.databaseFilesTable.selectedRows.length === 1) { + this.objectInfo.files?.splice(this.databaseFilesTable.selectedRows[0], 1); + var newData = this.objectInfo.files?.map(file => { + return this.convertToDataView(file); + }); + await this.setTableData(this.databaseFilesTable, newData, DefaultMaxTableRowCount) + } + } + + /** + * Validate the selected row to enable/disable the remove button + * @returns true if the remove button should be enabled, false otherwise + */ + protected override get removeButtonEnabled(): boolean { + let isEnabled = true; + if (this.databaseFilesTable.selectedRows !== undefined) { + const selectedRowId = this.objectInfo.files[this.databaseFilesTable.selectedRows[0]].id; + // Cannot delete a Primary row data file, Id is always 1. + if (this.databaseFilesTable.selectedRows.length === 1 && selectedRowId === 1) { + isEnabled = false; + } + // Cannot remove a log file if there are no other log files, LogFiletype is always a Log file type + else if (this.objectInfo.files[this.databaseFilesTable.selectedRows[0]].type === localizedConstants.LogFiletype) { + isEnabled = false; + this.objectInfo.files.forEach(file => { + if (file.id !== selectedRowId && file.type === localizedConstants.LogFiletype) { + isEnabled = true; + } + }); + } + } + return isEnabled; + } + + private async openDatabaseFileDialog(button: azdata.ButtonComponent): Promise { + const defaultFileSizeInMb: number = 8 + const defaultFileGrowthInMb: number = 64 + const defaultFileGrowthInPercent: number = 10; + const defaultMaxFileSizeLimitedToInMb: number = 100; + const selectedFile = this.databaseFilesTable.selectedRows !== undefined ? this.objectInfo.files[this.databaseFilesTable?.selectedRows[0]] : undefined; + if (!isUndefinedOrNull(selectedFile) && selectedFile.type === localizedConstants.FilestreamFileType) { + selectedFile.autoFileGrowth = defaultFileGrowthInMb; + } + const isNewFile: boolean = button.ariaLabel === localizedConstants.AddButton; + const isEditingNewFile: boolean = button.ariaLabel === localizedConstants.EditButton && selectedFile.id === undefined; + const databaseFile: DatabaseFile = isNewFile ? { + id: undefined, + name: '', + type: localizedConstants.RowsDataFileType, + path: this.objectInfo.files[0].path, + fileGroup: this.viewInfo.rowDataFileGroupsOptions[0], + fileNameWithExtension: '', + sizeInMb: defaultFileSizeInMb, + isAutoGrowthEnabled: true, + autoFileGrowth: defaultFileGrowthInMb, + autoFileGrowthType: FileGrowthType.KB, + maxSizeLimitInMb: defaultMaxFileSizeLimitedToInMb + } : selectedFile; + + const dialog = new DatabaseFileDialog({ + title: (isNewFile || isEditingNewFile) ? localizedConstants.AddDatabaseFilesText : localizedConstants.EditDatabaseFilesText(databaseFile.name), + viewInfo: this.viewInfo, + files: this.objectInfo.files, + isNewFile: isNewFile, + isEditingNewFile: isEditingNewFile, + databaseFile: databaseFile, + defaultFileConstants: { + defaultFileSizeInMb: defaultFileSizeInMb, + defaultFileGrowthInMb: defaultFileGrowthInMb, + defaultFileGrowthInPercent: defaultFileGrowthInPercent, + defaultMaxFileSizeLimitedToInMb: defaultMaxFileSizeLimitedToInMb + } + }); + await dialog.open(); + return await dialog.waitForClose(); + } + + //#endregion + //#region Database Properties - Options Tab private initializeOptionsGeneralSection(): void { let containers: azdata.Component[] = []; diff --git a/extensions/mssql/src/objectManagement/ui/databaseFileDialog.ts b/extensions/mssql/src/objectManagement/ui/databaseFileDialog.ts new file mode 100644 index 0000000000..200b84b7e8 --- /dev/null +++ b/extensions/mssql/src/objectManagement/ui/databaseFileDialog.ts @@ -0,0 +1,384 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as azdata from 'azdata'; +import * as vscode from 'vscode'; +import * as path from 'path'; +import { DefaultInputWidth, DialogBase } from '../../ui/dialogBase'; +import * as localizedConstants from '../localizedConstants'; +import { DatabaseFile, DatabaseViewInfo, FileGrowthType } from '../interfaces'; +import { isUndefinedOrNull } from '../../types'; +import { deepClone } from '../../util/objects'; + +export interface NewDatabaseFileDialogOptions { + title: string; + viewInfo: DatabaseViewInfo; + files: DatabaseFile[]; + isNewFile: boolean; + isEditingNewFile: boolean; + databaseFile: DatabaseFile; + defaultFileConstants: { + defaultFileSizeInMb: number, + defaultFileGrowthInPercent: number, + defaultFileGrowthInMb: number, + defaultMaxFileSizeLimitedToInMb: number + }; +} + +const fileSizeInputMaxValueInMbForDataType = 16776192; // Row type supports up to 16 TB (SSMS allows =~ 15.99TB) +const fileSizeInputMaxValueInMbForLogType = 2 * 1024 * 1024; // Row type supports up to 2 TB +const fileSizeInputMaxValueInPercent = 100; // SSMS allows more than 100, but we are limiting to 100 in ADS + +export class DatabaseFileDialog extends DialogBase { + private result: DatabaseFile; + private fileSizeInput: azdata.InputBoxComponent; + private fileNameWithExtension: azdata.InputBoxComponent; + private fileGroupDropdown: azdata.DropDownComponent; + private AutogrowthGroup: azdata.GroupContainer; + private fileGrowthGroup: azdata.GroupContainer; + private maxSizeGroup: azdata.GroupContainer; + private pathContainer: azdata.FlexContainer; + private enableAutoGrowthCheckbox: azdata.CheckBoxComponent; + private inPercentAutogrowth: azdata.RadioButtonComponent; + private inMegabytesAutogrowth: azdata.RadioButtonComponent; + private autoFilegrowthInput: azdata.InputBoxComponent; + private autogrowthInPercentValue: number; + private autogrowthInMegabytesValue: number; + private limitedToMbFileSize: azdata.RadioButtonComponent; + private unlimitedFileSize: azdata.RadioButtonComponent; + private limitedToMbFileSizeInput: azdata.InputBoxComponent; + private fileSizeValue: number; + protected filePathButton: azdata.ButtonComponent; + protected filePathTextBox: azdata.InputBoxComponent; + private originalName: string; + private originalFileName: string; + private isEditingFile: boolean; + + constructor(private readonly options: NewDatabaseFileDialogOptions) { + super(options.title, 'DatabaseFileDialog'); + } + + protected override async initialize(): Promise { + this.dialogObject.okButton.enabled = false; + this.autogrowthInPercentValue = this.options.defaultFileConstants.defaultFileGrowthInPercent; + this.autogrowthInMegabytesValue = this.options.defaultFileConstants.defaultFileGrowthInMb; + this.result = deepClone(this.options.databaseFile); + this.originalName = this.options.databaseFile.name; + this.originalFileName = this.options.databaseFile.fileNameWithExtension; + this.isEditingFile = this.options.isNewFile || this.options.isEditingNewFile; + await this.initializeAddDatabaseFileDialog(); + } + + /** + * Validates the file properties and returns an array of error messages + * @returns array of error messages if validation fails or empty array if validation succeeds + */ + protected override async validateInput(): Promise { + const errors = await super.validateInput(); + // Name validations + if (this.result.name !== this.originalName) { + // If adding a new file, can check if no exisiting file should have the same name + // If editing a new file, modified name should not be matched in the collection, if length != 0 means some other file has the name already + if ((this.options.isNewFile && !!this.options.files.find(file => { return file.name === this.result.name.trim() })) || + (this.options.isEditingNewFile && this.options.files.filter(file => { return file.name === this.result.name.trim() }).length !== 0)) { + errors.push(localizedConstants.DuplicateLogicalNameError(this.result.name.trim())); + } + // If editing existing file, current name should not be same as any other existing file + if (!this.options.isNewFile && !this.options.isEditingNewFile && !!this.options.files.find(file => { return this.result.id !== file.id && file.name === this.result.name.trim() })) { + errors.push(localizedConstants.FileNameExistsError(this.result.name.trim())); + } + // If new file, verify if the file name with extension already exists + if (this.options.isNewFile && !!this.options.files.find(file => { return (path.join(file.path, file.fileNameWithExtension) === path.join(this.result.path, this.result.fileNameWithExtension)) })) { + errors.push(localizedConstants.FileAlreadyExistsError(path.join(this.result.path, this.result.fileNameWithExtension))); + } + } + + // If editing a new file and the file name with extension is modified, verify if the file name with extension already exists + if (this.options.isEditingNewFile && this.result.fileNameWithExtension !== this.originalFileName) { + if (this.options.files.filter(file => { return (path.join(file.path, file.fileNameWithExtension)) === (path.join(this.result.path, this.result.fileNameWithExtension)) }).length !== 0) { + errors.push(localizedConstants.FileAlreadyExistsError(path.join(this.result.path, this.result.fileNameWithExtension))); + } + } + + // When maxsize is limited and size should not be greater than maxSize allowed + if (this.result.maxSizeLimitInMb !== -1 && this.result.maxSizeLimitInMb < this.result.sizeInMb) { + errors.push(localizedConstants.FileSizeLimitError); + } + // When maxsize is limited and fileGrowth should not be greater than maxSize allowed + if (this.result.maxSizeLimitInMb !== -1 && this.result.autoFileGrowthType !== FileGrowthType.Percent + && this.result.maxSizeLimitInMb < this.result.autoFileGrowth) { + errors.push(localizedConstants.FilegrowthLimitError); + } + return errors; + } + + private async initializeAddDatabaseFileDialog(): Promise { + let containers: azdata.Component[] = []; + // Logical Name of the file + const logicalname = this.createInputBox(async (newValue) => { + if (newValue.trim() !== '') { + this.result.name = newValue.trim(); + this.fileNameWithExtension.value = this.generateFileNameWithExtension(); + } + }, { + ariaLabel: localizedConstants.LogicalNameText, + inputType: 'text', + enabled: true, + value: this.options.databaseFile.name, + required: true + }); + const filenameContainer = this.createLabelInputContainer(localizedConstants.LogicalNameText, logicalname); + containers.push(filenameContainer); + + // File Type + const fileType = this.createDropdown(localizedConstants.FileTypeText, async (newValue) => { + await this.updateOptionsForSelectedFileType(newValue); + this.result.type = newValue; + this.fileNameWithExtension.value = this.generateFileNameWithExtension(); + }, this.options.viewInfo.fileTypesOptions, this.result.type, this.isEditingFile, DefaultInputWidth); + const fileTypeContainer = this.createLabelInputContainer(localizedConstants.FileTypeText, fileType); + containers.push(fileTypeContainer); + + // Filegroup + this.fileGroupDropdown = this.createDropdown(localizedConstants.FilegroupText, async (newValue) => { + this.result.fileGroup = newValue; + }, this.options.viewInfo.rowDataFileGroupsOptions, this.options.databaseFile.fileGroup, this.isEditingFile, DefaultInputWidth); + const sizeContainer = this.createLabelInputContainer(localizedConstants.FilegroupText, this.fileGroupDropdown); + containers.push(sizeContainer); + + // File Size in MB + this.fileSizeInput = this.createInputBox(async (newValue) => { + this.result.sizeInMb = Number(newValue); + }, { + ariaLabel: localizedConstants.SizeInMbText, + inputType: 'number', + enabled: this.options.databaseFile.type !== localizedConstants.FilestreamFileType, + value: String(this.options.databaseFile.sizeInMb) + }); + const fileSizeContainer = this.createLabelInputContainer(localizedConstants.SizeInMbText, this.fileSizeInput); + containers.push(fileSizeContainer); + + // Auto Growth and Max Size + containers.push(await this.initializeAutogrowthSection()); + + // Path + this.filePathTextBox = this.createInputBox(async (newValue) => { + this.result.path = newValue; + }, { + ariaLabel: localizedConstants.PathText, + inputType: 'text', + enabled: this.isEditingFile, + value: this.options.databaseFile.path, + width: DefaultInputWidth - 30 + }); + this.filePathButton = this.createButton('...', '...', async () => { await this.createFileBrowser() }, this.options.isNewFile); + this.filePathButton.width = 25; + this.pathContainer = this.createLabelInputContainer(localizedConstants.PathText, this.filePathTextBox); + this.pathContainer.addItems([this.filePathButton], { flex: '10 0 auto' }); + containers.push(this.pathContainer); + + // File Name + let fileNameEnabled = this.isEditingFile; + if (fileNameEnabled) { + fileNameEnabled = !(this.result.type === localizedConstants.FilestreamFileType); + } + this.fileNameWithExtension = this.createInputBox(async (newValue) => { + this.result.fileNameWithExtension = newValue; + }, { + ariaLabel: localizedConstants.FileNameText, + inputType: 'text', + enabled: fileNameEnabled, // false for edit old file and for filestream type + value: this.options.databaseFile.fileNameWithExtension, + width: DefaultInputWidth + }); + const fileNameWithExtensionContainer = this.createLabelInputContainer(localizedConstants.FileNameText, this.fileNameWithExtension); + containers.push(fileNameWithExtensionContainer); + + this.formContainer.addItems(containers); + } + + /** + * Initialized file growth and max file size sections + * @returns a group container with 'auto file growth' options + */ + private async initializeAutogrowthSection(): Promise { + // Autogrowth checkbox + this.enableAutoGrowthCheckbox = this.createCheckbox(localizedConstants.EnableAutogrowthText, async (checked) => { + this.inPercentAutogrowth.enabled + = this.inMegabytesAutogrowth.enabled + = this.autoFilegrowthInput.enabled + = this.limitedToMbFileSize.enabled + = this.limitedToMbFileSizeInput.enabled + = this.unlimitedFileSize.enabled + = this.result.isAutoGrowthEnabled = checked; + }, true, true); + + // Autogrowth radio button and input section + let radioGroupName = 'autogrowthRadioGroup'; + const isFileAutoGrowthInKB = this.options.databaseFile.autoFileGrowthType === FileGrowthType.KB; + this.inPercentAutogrowth = this.createRadioButton(localizedConstants.InPercentAutogrowthText, radioGroupName, !isFileAutoGrowthInKB, async (checked) => { await this.handleAutogrowthTypeChange(checked); }); + this.inMegabytesAutogrowth = this.createRadioButton(localizedConstants.InMegabytesAutogrowthText, radioGroupName, isFileAutoGrowthInKB, async (checked) => { await this.handleAutogrowthTypeChange(checked); }); + this.autoFilegrowthInput = this.createInputBox(async (newValue) => { + if (!isUndefinedOrNull(newValue) && newValue !== '') { + if (this.inPercentAutogrowth.checked) { + this.autogrowthInPercentValue = Number(newValue); + } else { + this.autogrowthInMegabytesValue = Number(newValue); + } + this.result.autoFileGrowth = this.inPercentAutogrowth.checked ? this.autogrowthInPercentValue : this.autogrowthInMegabytesValue; + } + }, { + ariaLabel: localizedConstants.FileGrowthText, + inputType: 'number', + enabled: true, + value: String(this.options.databaseFile.autoFileGrowth), + width: DefaultInputWidth - 10, + min: 1 + }); + const autogrowthContainer = this.createLabelInputContainer(localizedConstants.FileGrowthText, this.autoFilegrowthInput); + this.fileGrowthGroup = this.createGroup('', [this.enableAutoGrowthCheckbox + , autogrowthContainer, this.inPercentAutogrowth, this.inMegabytesAutogrowth], true); + await this.fileGrowthGroup.updateCssStyles({ 'margin': '10px 0px -10px -10px' }); + + // Autogrowth radio button and input section + radioGroupName = 'maxFileSizeRadioGroup'; + const isFileSizeLimited = this.options.isNewFile ? false : this.options.databaseFile.maxSizeLimitInMb !== -1; + this.limitedToMbFileSize = this.createRadioButton(localizedConstants.LimitedToMBFileSizeText, radioGroupName, isFileSizeLimited, async (checked) => { await this.handleMaxFileSizeTypeChange(checked); }); + this.unlimitedFileSize = this.createRadioButton(localizedConstants.UnlimitedFileSizeText, radioGroupName, !isFileSizeLimited, async (checked) => { await this.handleMaxFileSizeTypeChange(checked); }); + this.limitedToMbFileSizeInput = this.createInputBox(async (newValue) => { + this.fileSizeValue = Number(newValue); + this.result.maxSizeLimitInMb = this.fileSizeValue; + if (this.unlimitedFileSize.checked) { + this.result.maxSizeLimitInMb = -1; + } + }, { + ariaLabel: localizedConstants.MaximumFileSizeText, + inputType: 'number', + enabled: true, + value: this.options.databaseFile.maxSizeLimitInMb === -1 ? String(this.options.defaultFileConstants.defaultMaxFileSizeLimitedToInMb) : String(this.options.databaseFile.maxSizeLimitInMb), + width: DefaultInputWidth - 10, + min: 1, + max: this.options.databaseFile.type === localizedConstants.LogFiletype ? fileSizeInputMaxValueInMbForLogType : fileSizeInputMaxValueInMbForDataType + }); + const fileSizeContainer = this.createLabelInputContainer(localizedConstants.MaximumFileSizeText, this.limitedToMbFileSizeInput); + this.maxSizeGroup = this.createGroup('', [fileSizeContainer, this.limitedToMbFileSize, this.unlimitedFileSize], true); + await this.maxSizeGroup.updateCssStyles({ 'margin': '10px 0px -10px -10px' }); + + this.AutogrowthGroup = this.createGroup(localizedConstants.AutogrowthMaxsizeText, [this.fileGrowthGroup, this.maxSizeGroup], false); + return this.AutogrowthGroup; + } + + private async handleAutogrowthTypeChange(checked: boolean): Promise { + this.autoFilegrowthInput.value = this.options.isNewFile ? (this.inPercentAutogrowth.checked ? this.autogrowthInPercentValue?.toString() : this.autogrowthInMegabytesValue?.toString()) : this.options.databaseFile.autoFileGrowth?.toString(); + this.autoFilegrowthInput.max = this.inPercentAutogrowth.checked ? fileSizeInputMaxValueInPercent : (this.result.type === localizedConstants.LogFiletype ? fileSizeInputMaxValueInMbForLogType / 2 : fileSizeInputMaxValueInMbForDataType / 2); + this.result.autoFileGrowthType = this.inPercentAutogrowth.checked ? FileGrowthType.Percent : FileGrowthType.KB; + } + + private async handleMaxFileSizeTypeChange(checked: boolean): Promise { + if (this.limitedToMbFileSize.checked) { + this.limitedToMbFileSizeInput.enabled = true; + this.result.maxSizeLimitInMb = this.fileSizeValue; + } else if (this.unlimitedFileSize.checked) { + this.limitedToMbFileSizeInput.enabled = false; + this.result.maxSizeLimitInMb = -1; //Unlimited + } + } + + /** + * Creates a file browser and sets the path to the filePath + */ + private async createFileBrowser(): Promise { + let fileUris = await vscode.window.showOpenDialog( + { + canSelectFiles: false, + canSelectFolders: true, + canSelectMany: false, + defaultUri: vscode.Uri.file(this.options.databaseFile.path), + openLabel: localizedConstants.SelectText + } + ); + + if (!fileUris || fileUris.length === 0) { + return; + } + + let fileUri = fileUris[0]; + this.filePathTextBox.value = fileUri.fsPath; + this.result.path = fileUri.fsPath; + } + + /** + * Toggles fileGroup dropdown options and visibility of the autogrowth file group section based on the selected file type + * @param selectedOption the selected option from the fileType dropdown + */ + private async updateOptionsForSelectedFileType(selectedOption: string): Promise { + // Row Data defaults + let fileGroupDdOptions = this.options.viewInfo.rowDataFileGroupsOptions; + let fileGroupDdValue = this.result.fileGroup; + let visibility = 'visible'; + let maxSizeGroupMarginTop = '0px'; + let pathContainerMarginTop = '0px'; + let enableInputs = true; + let fileSizeInputMaxValue = fileSizeInputMaxValueInMbForDataType; + // Log + if (selectedOption === localizedConstants.LogFiletype) { + fileGroupDdOptions = [localizedConstants.FileGroupForLogTypeText]; + fileGroupDdValue = localizedConstants.FileGroupForLogTypeText; + fileSizeInputMaxValue = fileSizeInputMaxValueInMbForLogType + } + // File Stream + else if (selectedOption === localizedConstants.FilestreamFileType) { + fileGroupDdOptions = this.options.viewInfo.fileStreamFileGroupsOptions; + fileGroupDdValue = this.result.fileGroup; + visibility = 'hidden'; + maxSizeGroupMarginTop = '-130px'; + pathContainerMarginTop = '-35px'; + enableInputs = false; + this.fileNameWithExtension.value = ''; + } + + // Update the propertie + await this.fileGroupDropdown.updateProperties({ + values: fileGroupDdOptions, value: fileGroupDdValue + }); + await this.fileGrowthGroup.updateCssStyles({ 'visibility': visibility }); + await this.maxSizeGroup.updateCssStyles({ 'margin-top': maxSizeGroupMarginTop }); + await this.pathContainer.updateCssStyles({ 'margin-top': pathContainerMarginTop }); + this.fileNameWithExtension.enabled = this.fileSizeInput.enabled = this.isEditingFile && enableInputs; + this.autoFilegrowthInput.max = this.inPercentAutogrowth.checked ? fileSizeInputMaxValueInPercent : fileSizeInputMaxValue / 2; + this.fileSizeInput.max = fileSizeInputMaxValue; + } + + /** + * Generates the file name with extension on logical name update + */ + private generateFileNameWithExtension(): string { + let fileNameWithExtenstion = this.result.fileNameWithExtension; + // if new file, then update the generate the fileNameWithExtenison + if (this.result.name !== '' && this.options.isNewFile) { + switch (this.result.type) { + case localizedConstants.RowsDataFileType: + fileNameWithExtenstion = this.result.name + '.ndf'; + break; + case localizedConstants.LogFiletype: + fileNameWithExtenstion = this.result.name + '.ldf'; + break; + case localizedConstants.FilestreamFileType: + fileNameWithExtenstion = ''; + break; + } + } + return fileNameWithExtenstion; + } + + + public override async onFormFieldChange(): Promise { + this.dialogObject.okButton.enabled = JSON.stringify(this.result) !== JSON.stringify(this.options.databaseFile); + } + + protected override get dialogResult(): DatabaseFile | undefined { + return this.result; + } +} diff --git a/extensions/mssql/src/objectManagement/ui/databaseRoleDialog.ts b/extensions/mssql/src/objectManagement/ui/databaseRoleDialog.ts index b4d7454b65..ee1732a286 100644 --- a/extensions/mssql/src/objectManagement/ui/databaseRoleDialog.ts +++ b/extensions/mssql/src/objectManagement/ui/databaseRoleDialog.ts @@ -91,26 +91,32 @@ export class DatabaseRoleDialog extends PrincipalDialogBase [m])); - const buttonContainer = this.addButtonsForTable(this.memberTable, localizedConstants.AddMemberAriaLabel, localizedConstants.RemoveMemberAriaLabel, - async () => { - const dialog = new FindObjectDialog(this.objectManagementService, { - objectTypes: localizedConstants.getObjectTypeInfo([ - ObjectManagement.NodeType.DatabaseRole, - ObjectManagement.NodeType.User - ]), - selectAllObjectTypes: true, - multiSelect: true, - contextId: this.contextId, - title: localizedConstants.SelectDatabaseRoleMemberDialogTitle, - showSchemaColumn: false - }); - await dialog.open(); - const result = await dialog.waitForClose(); - await this.addMembers(result.selectedObjects.map(r => r.name)); + const buttonContainer = this.addButtonsForTable(this.memberTable, + { + buttonAriaLabel: localizedConstants.AddMemberAriaLabel, + buttonHandler: async () => { + const dialog = new FindObjectDialog(this.objectManagementService, { + objectTypes: localizedConstants.getObjectTypeInfo([ + ObjectManagement.NodeType.DatabaseRole, + ObjectManagement.NodeType.User + ]), + selectAllObjectTypes: true, + multiSelect: true, + contextId: this.contextId, + title: localizedConstants.SelectDatabaseRoleMemberDialogTitle, + showSchemaColumn: false + }); + await dialog.open(); + const result = await dialog.waitForClose(); + await this.addMembers(result.selectedObjects.map(r => r.name)); + } }, - async () => { - if (this.memberTable.selectedRows.length === 1) { - await this.removeMember(this.memberTable.selectedRows[0]); + { + buttonAriaLabel: localizedConstants.RemoveMemberAriaLabel, + buttonHandler: async () => { + if (this.memberTable.selectedRows.length === 1) { + await this.removeMember(this.memberTable.selectedRows[0]); + } } }); this.memberSection = this.createGroup(localizedConstants.MemberSectionHeader, [this.memberTable, buttonContainer]); diff --git a/extensions/mssql/src/objectManagement/ui/detachDatabaseDialog.ts b/extensions/mssql/src/objectManagement/ui/detachDatabaseDialog.ts index 49db828dd2..32c792026b 100644 --- a/extensions/mssql/src/objectManagement/ui/detachDatabaseDialog.ts +++ b/extensions/mssql/src/objectManagement/ui/detachDatabaseDialog.ts @@ -23,7 +23,7 @@ export class DetachDatabaseDialog extends ObjectManagementDialogBase { - let tableData = this.viewInfo.files.map(file => [file.name, file.type, file.fileGroup, file.path]); + let tableData = this.objectInfo.files.map(file => [file.name, file.type, file.fileGroup, file.path]); let columnNames = [DatabaseFileNameLabel, DatabaseFileTypeLabel, DatabaseFileGroupLabel, DatabaseFilePathLabel]; let fileTable = this.createTable(DatabaseFilesLabel, columnNames, tableData); let tableGroup = this.createGroup(DatabaseFilesLabel, [fileTable], false); diff --git a/extensions/mssql/src/objectManagement/ui/principalDialogBase.ts b/extensions/mssql/src/objectManagement/ui/principalDialogBase.ts index 4750b2d66a..93cdee2fc6 100644 --- a/extensions/mssql/src/objectManagement/ui/principalDialogBase.ts +++ b/extensions/mssql/src/objectManagement/ui/principalDialogBase.ts @@ -10,7 +10,7 @@ import * as localizedConstants from '../localizedConstants'; import { ObjectManagementDialogBase, ObjectManagementDialogOptions } from './objectManagementDialogBase'; import { FindObjectDialog, FindObjectDialogResult } from './findObjectDialog'; import { deepClone } from '../../util/objects'; -import { DefaultTableWidth, getTableHeight } from '../../ui/dialogBase'; +import { DefaultTableWidth, DialogButton, getTableHeight } from '../../ui/dialogBase'; import { ObjectSelectionMethod, ObjectSelectionMethodDialog } from './objectSelectionMethodDialog'; import { DatabaseLevelPrincipalViewInfo, SecurablePermissionItem, SecurablePermissions, SecurityPrincipalObject, SecurityPrincipalViewInfo } from '../interfaces'; @@ -51,8 +51,15 @@ export abstract class PrincipalDialogBase this.onAddSecurableButtonClicked(button), () => this.onRemoveSecurableButtonClicked()); + const addButtonComponent: DialogButton = { + buttonAriaLabel: localizedConstants.AddSecurableAriaLabel, + buttonHandler: (button) => this.onAddSecurableButtonClicked(button) + }; + const removeButtonComponent: DialogButton = { + buttonAriaLabel: localizedConstants.RemoveSecurableAriaLabel, + buttonHandler: () => this.onRemoveSecurableButtonClicked() + }; + const buttonContainer = this.addButtonsForTable(this.securableTable, addButtonComponent, removeButtonComponent); this.disposables.push(this.securableTable.onRowSelected(async () => { await this.updatePermissionsTable(); })); diff --git a/extensions/mssql/src/objectManagement/ui/serverRoleDialog.ts b/extensions/mssql/src/objectManagement/ui/serverRoleDialog.ts index 99955502b2..b6ea85a86b 100644 --- a/extensions/mssql/src/objectManagement/ui/serverRoleDialog.ts +++ b/extensions/mssql/src/objectManagement/ui/serverRoleDialog.ts @@ -96,27 +96,32 @@ export class ServerRoleDialog extends PrincipalDialogBase [m])); - const buttonContainer = this.addButtonsForTable(this.memberTable, localizedConstants.AddMemberAriaLabel, localizedConstants.RemoveMemberAriaLabel, - async () => { - const dialog = new FindObjectDialog(this.objectManagementService, { - objectTypes: localizedConstants.getObjectTypeInfo([ - ObjectManagement.NodeType.ServerLevelLogin, - ObjectManagement.NodeType.ServerLevelServerRole - ]), - selectAllObjectTypes: true, - multiSelect: true, - contextId: this.contextId, - title: localizedConstants.SelectServerRoleMemberDialogTitle - }); - await dialog.open(); - const result = await dialog.waitForClose(); - await this.addMembers(result.selectedObjects.map(r => r.name)); - }, - async () => { + const buttonContainer = this.addButtonsForTable(this.memberTable, + { + buttonAriaLabel: localizedConstants.AddMemberAriaLabel, + buttonHandler: async () => { + const dialog = new FindObjectDialog(this.objectManagementService, { + objectTypes: localizedConstants.getObjectTypeInfo([ + ObjectManagement.NodeType.ServerLevelLogin, + ObjectManagement.NodeType.ServerLevelServerRole + ]), + selectAllObjectTypes: true, + multiSelect: true, + contextId: this.contextId, + title: localizedConstants.SelectServerRoleMemberDialogTitle + }); + await dialog.open(); + const result = await dialog.waitForClose(); + await this.addMembers(result.selectedObjects.map(r => r.name)); + } + }, { + buttonAriaLabel: localizedConstants.RemoveMemberAriaLabel, + buttonHandler: async () => { if (this.memberTable.selectedRows.length === 1) { await this.removeMember(this.memberTable.selectedRows[0]); } - }); + } + }); this.memberSection = this.createGroup(localizedConstants.MemberSectionHeader, [this.memberTable, buttonContainer]); } diff --git a/extensions/mssql/src/ui/dialogBase.ts b/extensions/mssql/src/ui/dialogBase.ts index b618d17c64..a2ab263f85 100644 --- a/extensions/mssql/src/ui/dialogBase.ts +++ b/extensions/mssql/src/ui/dialogBase.ts @@ -21,6 +21,11 @@ export function getTableHeight(rowCount: number, minRowCount: number = DefaultMi return Math.min(Math.max(rowCount, minRowCount), maxRowCount) * TableRowHeight + TableColumnHeaderHeight; } +export interface DialogButton { + buttonAriaLabel: string; + buttonHandler: (button: azdata.ButtonComponent) => Promise +} + export type TableListItemEnabledStateGetter = (item: T) => boolean; export type TableListItemValueGetter = (item: T) => string[]; export type TableListItemComparer = (item1: T, item2: T) => boolean; @@ -72,6 +77,8 @@ export abstract class DialogBase { protected onFormFieldChange(): void { } + protected get removeButtonEnabled(): boolean { return true; } + protected validateInput(): Promise { return Promise.resolve([]); } public async open(): Promise { @@ -155,7 +162,7 @@ export abstract class DialogBase { } /** - * Creates an input box. If properties are not passed in, then an input box is created with the following default properties: + * Creates an input box. If properties are not passed in, then an input box is created with the following default properties: * inputType - text * width - DefaultInputWidth * value - empty @@ -270,28 +277,48 @@ export abstract class DialogBase { return table; } - protected addButtonsForTable(table: azdata.TableComponent, addButtonAriaLabel: string, removeButtonAriaLabel: string, addHandler: (button: azdata.ButtonComponent) => Promise, removeHandler: (button: azdata.ButtonComponent) => Promise): azdata.FlexContainer { - let addButton: azdata.ButtonComponent; - let removeButton: azdata.ButtonComponent; - const updateButtons = () => { + protected addButtonsForTable(table: azdata.TableComponent, addbutton: DialogButton, removeButton: DialogButton, editButton: DialogButton = undefined): azdata.FlexContainer { + let addButtonComponent: azdata.ButtonComponent; + let editButtonComponent: azdata.ButtonComponent; + let removeButtonComponent: azdata.ButtonComponent; + let buttonComponents: azdata.ButtonComponent[] = []; + const updateButtons = (isRemoveEnabled: boolean = undefined) => { this.onFormFieldChange(); - removeButton.enabled = table.selectedRows?.length === 1 && table.selectedRows[0] !== -1 && table.selectedRows[0] < table.data.length; + const tableSelectedRowsLengthCheck = table.selectedRows?.length === 1 && table.selectedRows[0] !== -1 && table.selectedRows[0] < table.data.length; + if (editButton !== undefined) { + editButtonComponent.enabled = tableSelectedRowsLengthCheck; + } + removeButtonComponent.enabled = !!isRemoveEnabled && tableSelectedRowsLengthCheck; } - addButton = this.createButton(uiLoc.AddText, addButtonAriaLabel, async () => { - await addHandler(addButton); + addButtonComponent = this.createButton(uiLoc.AddText, addbutton.buttonAriaLabel, async () => { + await addbutton.buttonHandler(addButtonComponent); updateButtons(); }); - removeButton = this.createButton(uiLoc.RemoveText, removeButtonAriaLabel, async () => { - await removeHandler(removeButton); + buttonComponents.push(addButtonComponent); + + if (editButton !== undefined) { + editButtonComponent = this.createButton(uiLoc.EditText, editButton.buttonAriaLabel, async () => { + await editButton.buttonHandler(editButtonComponent); + updateButtons(); + }, false); + buttonComponents.push(editButtonComponent); + } + + removeButtonComponent = this.createButton(uiLoc.RemoveText, removeButton.buttonAriaLabel, async () => { + await removeButton.buttonHandler(removeButtonComponent); if (table.selectedRows.length === 1 && table.selectedRows[0] >= table.data.length) { table.selectedRows = [table.data.length - 1]; } updateButtons(); }, false); + buttonComponents.push(removeButtonComponent); + this.disposables.push(table.onRowSelected(() => { - updateButtons(); + const isRemoveButtonEnabled = this.removeButtonEnabled; + updateButtons(isRemoveButtonEnabled); })); - return this.createButtonContainer([addButton, removeButton]); + + return this.createButtonContainer(buttonComponents) } protected createDropdown(ariaLabel: string, handler: (newValue: string) => Promise, values: string[], value: string | undefined, enabled: boolean = true, width: number = DefaultInputWidth, editable?: boolean, strictSelection?: boolean): azdata.DropDownComponent { @@ -344,11 +371,12 @@ export abstract class DialogBase { }).withItems(items, { flex: '0 0 auto' }).component(); } - protected createRadioButton(label: string, groupName: string, checked: boolean, handler: (checked: boolean) => Promise): azdata.RadioButtonComponent { + protected createRadioButton(label: string, groupName: string, checked: boolean, handler: (checked: boolean) => Promise, enabled: boolean = true): azdata.RadioButtonComponent { const radio = this.modelView.modelBuilder.radioButton().withProps({ label: label, name: groupName, - checked: checked + checked: checked, + enabled: enabled }).component(); this.disposables.push(radio.onDidChangeCheckedState(async checked => { await handler(checked); diff --git a/extensions/mssql/src/ui/localizedConstants.ts b/extensions/mssql/src/ui/localizedConstants.ts index 2ff0c53d55..af35616b9e 100644 --- a/extensions/mssql/src/ui/localizedConstants.ts +++ b/extensions/mssql/src/ui/localizedConstants.ts @@ -14,6 +14,7 @@ export const LoadingDialogCompletedText: string = localize('mssql.ui.loadingDial export const ScriptText: string = localize('mssql.ui.scriptText', "Script"); export const SelectText = localize('objectManagement.selectLabel', "Select"); export const AddText = localize('objectManagement.addText', "Add…"); +export const EditText = localize('objectManagement.editText', "Edit"); export const RemoveText = localize('objectManagement.removeText', "Remove"); export const NoActionScriptedMessage: string = localize('mssql.ui.noActionScriptedMessage', "There is no action to be scripted."); export const ScriptGeneratedText: string = localize('mssql.ui.scriptGenerated', "Script has been generated successfully. You can close the dialog to view it in the newly opened editor.")