Enabling Files tab to the database properties (#24138)

* initial changes for loadin dsc table with real values from smo

* Displaying diff columns for DSC for diff sql server

* checkbox maiants the selection

* elevate option fails to load correct value when set to when_supported option

* all working till maxdop, todo pause option, save

* commented MAXDOP changes, as it is causing issues

* primary,sec,checkbox working as expected, TODO:MaxDop etc options,saving,tests

* Undo MAXDOP commented code

* refactored with service data

* column header width adjustments

* Maxdop and pause resume options completed, apply button is failing now

* Removed option names from loc  and using Id instead as names may change in future like in doc

* Apply button fixed

* refactored to reduce table reload

* Ledger digest completed

* refactor done: maxdop secondary shows wrong data from pause_resume

* refactor more: all working but table focus disturbs on update table

* adds conditions for unsupported dsc to <2016 server

* maxdop secondary checkbox fix

* rows still loses focus after value change due to update table row data

* Fixed updating secondary dropdown value

* reusing the private method and removed the duplicated codes

* initial commit - fullText and owner need revision

* Enter key in input type allows the change to update the table data, reduces the live update issues

* Setting focus to the current row

* loading data, need stylings-increase col length, etc

* using the existed setTableData method

* Adding new file dialog

* creating addFile, but not displaying in table, issue with appendData

* Adding row to the table, options are getting from STS

* all working except InPercent value

* code review comment updates

* Input type checkbox update table additional validation

* all except path

* fixing the input type focus and reverting the enterKeyPress logic

* browse path is created, need stylings,refactor,filestream selection and add

* fixing the flickering issue with data refresh

* new file options toggle and grid display string updates

* moving code inline and using actual component

* cleanup

* Add file saving is done, except one styling issue with autogrowth section

* add,remove working, need to edit file

* add, edit, remove - all working, need css fixes and -1 fix

* addressing code review comments

* adding local changes adn fixing for edit file

* adjusting css

* addressing code review comment for using loc var instead of duplicated line of code to get the rowinfo

* all fixed, need testing and refactor

* vBump STS  and fixing required field causing the apply button not enable for other options on main branch

* fixing autogrowth radio buttons change updates

* all working except some css

* disabled size for filestream

* fixing filegroups and filetypes scnearios, added filename validation for newfile, todo:editingNew file

* added max and min values to the inputs

* editing filename validation completed, all done exccept CSS

* all fixed except scroll bar

* edit db file header, filename enable issue fix

* PR comment supporting updates for STS

* min updates

* modfying addButtonsForTable method and reusing it for edit button

* code review comment updates

* Dialogbase addbuttons to table refactored

* more typo fixes

* removing fulltext index prop

* service fix

* using path.join instead of hardcoded separators

* final commit changes
This commit is contained in:
Sai Avishkar Sreerama
2023-08-29 14:42:09 -05:00
committed by GitHub
parent 9557e77982
commit c4b1765745
12 changed files with 795 additions and 65 deletions

View File

@@ -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',

View File

@@ -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<Database> {
@@ -466,7 +468,6 @@ export interface DatabaseViewInfo extends ObjectManagement.ObjectViewInfo<Databa
compatibilityLevels?: OptionsCollection;
containmentTypes?: OptionsCollection;
recoveryModels?: OptionsCollection;
files?: DatabaseFile[];
azureBackupRedundancyLevels?: string[];
azureServiceLevelObjectives?: AzureEditionDetails[];
azureEditions?: string[];
@@ -476,6 +477,9 @@ export interface DatabaseViewInfo extends ObjectManagement.ObjectViewInfo<Databa
dscOnOffOptions?: string[];
dscElevateOptions?: string[];
dscEnableDisableOptions?: string[];
rowDataFileGroupsOptions?: string[];
fileStreamFileGroupsOptions?: string[];
fileTypesOptions?: string[];
}
export interface DatabaseScopedConfigurationsInfo {
@@ -545,9 +549,22 @@ export interface NumericServerProperty {
export interface ServerViewInfo extends ObjectManagement.ObjectViewInfo<Server> {
}
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
}

View File

@@ -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 {

View File

@@ -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 },
]
}
}

View File

@@ -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, DatabaseViewInfo> {
// 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<Database, Databas
private memoryAllocatedInput: azdata.InputBoxComponent;
private memoryUsedInput: azdata.InputBoxComponent;
private collationInput: azdata.InputBoxComponent;
// Files Tab
private readonly filesTabId: string = 'filesDatabaseId';
private databaseFilesTable: azdata.TableComponent;
// Options Tab
private readonly optionsTabId: string = 'optionsDatabaseId';
private autoCreateIncrementalStatisticsInput: azdata.CheckBoxComponent;
@@ -91,6 +96,9 @@ export class DatabaseDialog extends ObjectManagementDialogBase<Database, Databas
case this.generalTabId:
helpUrl = DatabaseGeneralPropertiesDocUrl;
break;
case this.filesTabId:
helpUrl = DatabaseFilesPropertiesDocUrl;
break;
case this.optionsTabId:
helpUrl = DatabaseOptionsPropertiesDocUrl;
break;
@@ -113,11 +121,11 @@ export class DatabaseDialog extends ObjectManagementDialogBase<Database, Databas
}
this.formContainer.addItems(components);
} else {
// Initilaize general Tab sections
// Initialize general Tab sections
this.initializeBackupSection();
this.initializeDatabaseSection();
//Initilaize options Tab sections
//Initialize options Tab sections
this.initializeOptionsGeneralSection();
this.initializeAutomaticSection();
if (!isUndefinedOrNull(this.objectInfo.isLedgerDatabase)) {
@@ -130,7 +138,7 @@ export class DatabaseDialog extends ObjectManagementDialogBase<Database, Databas
const tabs: azdata.Tab[] = [];
// Initilaize general Tab
// Initialize general Tab
this.generalTab = {
title: localizedConstants.GeneralSectionHeader,
id: this.generalTabId,
@@ -141,7 +149,20 @@ export class DatabaseDialog extends ObjectManagementDialogBase<Database, Databas
};
tabs.push(this.generalTab);
// Initilaize Options Tab
// Initialize Files Tab
// Files tab is only enabled for SQL Server properties view
if (!isUndefinedOrNull(this.objectInfo.isFilesTabSupported)) {
const filesGeneralSection = this.initializeFilesGeneralSection();
const databaseFilesSection = this.initializeDatabaseFilesSection();
this.filesTab = {
title: localizedConstants.FilesSectionHeader,
id: this.filesTabId,
content: this.createGroup('', [filesGeneralSection, databaseFilesSection], false)
};
tabs.push(this.filesTab);
}
// Initialize Options Tab
this.optionsTab = {
title: localizedConstants.OptionsSectionHeader,
id: this.optionsTabId,
@@ -149,7 +170,7 @@ export class DatabaseDialog extends ObjectManagementDialogBase<Database, Databas
};
tabs.push(this.optionsTab);
// Initilaize DSC Tab section
// Initialize DSC Tab section
if (!isUndefinedOrNull(this.objectInfo.databaseScopedConfigurations)) {
await this.initializeDatabaseScopedConfigurationSection();
this.dscTabSectionsContainer.push(await this.initializeDscValueDropdownTypeSection())
@@ -162,7 +183,7 @@ export class DatabaseDialog extends ObjectManagementDialogBase<Database, Databas
tabs.push(this.dscTab);
}
// Initilaize tab group with tabbed panel
// Initialize tab group with tabbed panel
const propertiesTabGroup = { title: '', tabs: tabs };
const propertiesTabbedPannel = this.modelView.modelBuilder.tabbedPanel()
.withTabs([propertiesTabGroup])
@@ -396,6 +417,216 @@ export class DatabaseDialog extends ObjectManagementDialogBase<Database, Databas
}
//#endregion
//#region Database Properties - Files Tab
private initializeFilesGeneralSection(): azdata.GroupContainer {
let containers: azdata.Component[] = [];
// Database name
this.nameInput = this.createInputBox(async () => { }, {
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 <default> 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<void> {
// 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<void> {
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<void> {
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<DatabaseFile> {
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[] = [];

View File

@@ -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<DatabaseFile> {
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<void> {
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<string[]> {
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<void> {
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<azdata.GroupContainer> {
// 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<void> {
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<void> {
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<void> {
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<void> {
// 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<void> {
this.dialogObject.okButton.enabled = JSON.stringify(this.result) !== JSON.stringify(this.options.databaseFile);
}
protected override get dialogResult(): DatabaseFile | undefined {
return this.result;
}
}

View File

@@ -91,26 +91,32 @@ export class DatabaseRoleDialog extends PrincipalDialogBase<DatabaseRoleInfo, Da
private initializeMemberSection(): void {
this.memberTable = this.createTable(localizedConstants.MemberSectionHeader, [localizedConstants.NameText], this.objectInfo.members.map(m => [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]);

View File

@@ -23,7 +23,7 @@ export class DetachDatabaseDialog extends ObjectManagementDialogBase<Database, D
}
protected async initializeUI(): Promise<void> {
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);

View File

@@ -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<ObjectInfoType extends SecurityPrincip
securableTableColumns.splice(1, 0, localizedConstants.SchemaText);
}
this.securableTable = this.createTable(localizedConstants.SecurablesText, securableTableColumns, this.getSecurableTableData());
const buttonContainer = this.addButtonsForTable(this.securableTable, localizedConstants.AddSecurableAriaLabel, localizedConstants.RemoveSecurableAriaLabel,
(button) => 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();
}));

View File

@@ -96,27 +96,32 @@ export class ServerRoleDialog extends PrincipalDialogBase<ServerRoleInfo, Server
private initializeMemberSection(): void {
this.memberTable = this.createTable(localizedConstants.MemberSectionHeader, [localizedConstants.NameText], this.objectInfo.members.map(m => [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]);
}

View File

@@ -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<void>
}
export type TableListItemEnabledStateGetter<T> = (item: T) => boolean;
export type TableListItemValueGetter<T> = (item: T) => string[];
export type TableListItemComparer<T> = (item1: T, item2: T) => boolean;
@@ -72,6 +77,8 @@ export abstract class DialogBase<DialogResult> {
protected onFormFieldChange(): void { }
protected get removeButtonEnabled(): boolean { return true; }
protected validateInput(): Promise<string[]> { return Promise.resolve([]); }
public async open(): Promise<void> {
@@ -155,7 +162,7 @@ export abstract class DialogBase<DialogResult> {
}
/**
* 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<DialogResult> {
return table;
}
protected addButtonsForTable(table: azdata.TableComponent, addButtonAriaLabel: string, removeButtonAriaLabel: string, addHandler: (button: azdata.ButtonComponent) => Promise<void>, removeHandler: (button: azdata.ButtonComponent) => Promise<void>): 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<void>, 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<DialogResult> {
}).withItems(items, { flex: '0 0 auto' }).component();
}
protected createRadioButton(label: string, groupName: string, checked: boolean, handler: (checked: boolean) => Promise<void>): azdata.RadioButtonComponent {
protected createRadioButton(label: string, groupName: string, checked: boolean, handler: (checked: boolean) => Promise<void>, 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);

View File

@@ -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.")