From c8ed14f4a31be8345b2c59ac95a3390cb02bf625 Mon Sep 17 00:00:00 2001 From: Sai Avishkar Sreerama <74571829+ssreerama@users.noreply.github.com> Date: Thu, 22 Jun 2023 10:30:14 -0500 Subject: [PATCH] Adding database properties general tab with read-only fields (#23318) * Initial commit for adding a basic general tab for the database properties * Refactoring for dialog inputs * removed properties nodeType and using database node and additional cleanup, diabling the functionality. * Changes according to STS data fetch * Reuse database Dialog * Undo contract file change * more refactoring * fetched scrollbar fix into this PR * Tabbed panel is being used for horizontal tabs * stying fix for general tab button * final commit for today :) * Updates according to STS changes * missed updates * Refactored updates * moved options as discussed and added collapsible sections... need to fix scroll bar * Fixing the horizontal scroll bar of tabbedpanel --- extensions/mssql/package.json | 5 + .../mssql/src/objectManagement/commands.ts | 2 +- .../mssql/src/objectManagement/constants.ts | 1 + .../mssql/src/objectManagement/interfaces.ts | 10 +- .../objectManagement/localizedConstants.ts | 16 +++ .../objectManagementService.ts | 36 ++++- .../src/objectManagement/ui/databaseDialog.ts | 132 ++++++++++++++++-- .../mssql/src/objectManagement/utils.ts | 5 + 8 files changed, 192 insertions(+), 15 deletions(-) diff --git a/extensions/mssql/package.json b/extensions/mssql/package.json index 3a365e7d048b..91255db2f3fe 100644 --- a/extensions/mssql/package.json +++ b/extensions/mssql/package.json @@ -522,6 +522,11 @@ "group": "0_query@1", "isDefault": true }, + { + "command": "mssql.objectProperties", + "when": "connectionProvider == MSSQL && serverInfo && !isCloud && nodeType && nodeType =~ /^(Database|Server)$/ && mssql:engineedition != 11 && isDevelopment", + "group": "0_query@1" + }, { "command": "mssql.deleteObject", "when": "connectionProvider == MSSQL && nodeType =~ /^(ServerLevelLogin|User|ServerLevelServerRole|ApplicationRole|DatabaseRole|Database)$/ && config.workbench.enablePreviewFeatures", diff --git a/extensions/mssql/src/objectManagement/commands.ts b/extensions/mssql/src/objectManagement/commands.ts index a795429c388b..0994560074d8 100644 --- a/extensions/mssql/src/objectManagement/commands.ts +++ b/extensions/mssql/src/objectManagement/commands.ts @@ -106,7 +106,7 @@ async function handleObjectPropertiesDialogCommand(context: azdata.ObjectExplore return; } try { - const parentUrn = await getParentUrn(context); + const parentUrn = context.nodeInfo ? await getParentUrn(context) : undefined; const options: ObjectManagementDialogOptions = { connectionUri: connectionUri, isNewObject: false, diff --git a/extensions/mssql/src/objectManagement/constants.ts b/extensions/mssql/src/objectManagement/constants.ts index ef4255445f91..fc8b63a08bdf 100644 --- a/extensions/mssql/src/objectManagement/constants.ts +++ b/extensions/mssql/src/objectManagement/constants.ts @@ -28,6 +28,7 @@ export const AlterApplicationRoleDocUrl = 'https://learn.microsoft.com/sql/t-sql export const CreateDatabaseRoleDocUrl = 'https://learn.microsoft.com/sql/t-sql/statements/create-role-transact-sql'; export const AlterDatabaseRoleDocUrl = 'https://learn.microsoft.com/sql/t-sql/statements/alter-role-transact-sql'; export const CreateDatabaseDocUrl = 'https://learn.microsoft.com/sql/t-sql/statements/create-database-transact-sql'; +export const DatabasePropertiesDocUrl = 'https://learn.microsoft.com/sql/relational-databases/databases/database-properties-general-page'; export const enum TelemetryActions { CreateObject = 'CreateObject', diff --git a/extensions/mssql/src/objectManagement/interfaces.ts b/extensions/mssql/src/objectManagement/interfaces.ts index db511e37f2fb..eabb78239064 100644 --- a/extensions/mssql/src/objectManagement/interfaces.ts +++ b/extensions/mssql/src/objectManagement/interfaces.ts @@ -430,7 +430,15 @@ export interface Database extends ObjectManagement.SqlObject { recoveryModel?: string; compatibilityLevel?: string; containmentType?: string; - + dateCreated?: string; + lastDatabaseBackup?: string; + lastDatabaseLogBackup?: string; + memoryAllocatedToMemoryOptimizedObjectsInMb?: number; + memoryUsedByMemoryOptimizedObjectsInMb?: number; + numberOfUsers?: number; + sizeInMb?: number; + spaceAvailableInMb?: number; + status?: string; azureBackupRedundancyLevel?: string; azureServiceLevelObjective?: string; azureEdition?: string; diff --git a/extensions/mssql/src/objectManagement/localizedConstants.ts b/extensions/mssql/src/objectManagement/localizedConstants.ts index 3b012d77117b..4d17a00dcb20 100644 --- a/extensions/mssql/src/objectManagement/localizedConstants.ts +++ b/extensions/mssql/src/objectManagement/localizedConstants.ts @@ -222,6 +222,22 @@ export const ObjectSelectionMethodDialog_AllObjectsOfTypes = localize('objectMan export const ObjectSelectionMethodDialog_AllObjectsOfSchema = localize('objectManagement.ObjectSelectionMethodDialog_AllObjectsOfSchema', "All objects belonging to a schema"); export const ObjectSelectionMethodDialog_SelectSchemaDropdownLabel = localize('objectManagement.ObjectSelectionMethodDialog_SelectSchemaDropdownLabel', "Schema"); +//Database properties Dialog +export const LastDatabaseBackupText = localize('objectManagement.lastDatabaseBackup', "Last Database Backup"); +export const LastDatabaseLogBackupText = localize('objectManagement.lastDatabaseLogBackup', "Last Database Log Backup"); +export const BackupSectionHeader = localize('objectManagement.databaseProperties.backupSectionHeader', "Backup"); +export const DatabaseSectionHeader = localize('objectManagement.databaseProperties.databaseSectionHeader', "Database"); +export const NamePropertyText = localize('objectManagement.databaseProperties.name', "Name"); +export const StatusText = localize('objectManagement.databaseProperties.status', "Status"); +export const OwnerPropertyText = localize('objectManagement.databaseProperties.owner', "Owner"); +export const DateCreatedText = localize('objectManagement.databaseProperties.dateCreated', "Date Created"); +export const SizeText = localize('objectManagement.databaseProperties.size', "Size"); +export const SpaceAvailableText = localize('objectManagement.databaseProperties.spaceAvailable', "Space Available"); +export const NumberOfUsersText = localize('objectManagement.databaseProperties.numberOfUsers', "Number of Users"); +export const MemoryAllocatedText = localize('objectManagement.databaseProperties.memoryAllocated', "Memory Allocated To Memory Optimized Objects"); +export const MemoryUsedText = localize('objectManagement.databaseProperties.memoryUsed', "Memory Used By Memory Optimized Objects"); +export const StringValueInMB = (value: string) => localize('objectManagement.databaseProperties.mbUnitText', "{0} MB", value); + // Util functions export function getNodeTypeDisplayName(type: string, inTitle: boolean = false): string { switch (type) { diff --git a/extensions/mssql/src/objectManagement/objectManagementService.ts b/extensions/mssql/src/objectManagement/objectManagementService.ts index 6872c378314b..b08970010b6c 100644 --- a/extensions/mssql/src/objectManagement/objectManagementService.ts +++ b/extensions/mssql/src/objectManagement/objectManagementService.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ApplicationRoleViewInfo, AuthenticationType, DatabaseRoleViewInfo, LoginViewInfo, SecurablePermissions, SecurableTypeMetadata, ServerRoleViewInfo, User, UserType, UserViewInfo } from './interfaces'; +import { ApplicationRoleViewInfo, AuthenticationType, DatabaseRoleViewInfo, DatabaseViewInfo, LoginViewInfo, SecurablePermissions, SecurableTypeMetadata, ServerRoleViewInfo, User, UserType, UserViewInfo } from './interfaces'; import * as Utils from '../utils'; import * as constants from '../constants'; import * as contracts from '../contracts'; @@ -194,6 +194,8 @@ export class TestObjectManagementService implements IObjectManagementService { obj = this.getApplicationRoleView(isNewObject, objectUrn); } else if (objectType === ObjectManagement.NodeType.DatabaseRole) { obj = this.getDatabaseRoleView(isNewObject, objectUrn); + } else if (objectType === ObjectManagement.NodeType.Database) { + obj = this.getDatabaseView(isNewObject, objectUrn); } else if (objectType === ObjectManagement.NodeType.ServerLevelLogin) { obj = this.getLoginView(isNewObject, objectUrn); } else if (objectType === ObjectManagement.NodeType.ServerLevelServerRole) { @@ -432,6 +434,38 @@ export class TestObjectManagementService implements IObjectManagementService { }; } + private getDatabaseView(isNewObject: boolean, name: string): DatabaseViewInfo { + return isNewObject ? { + objectInfo: { + name: 'New Database Name', + owner: '', + collationName: '', + compatibilityLevel: '', + containmentType: '', + recoveryModel: '', + azureEdition: '', + azureMaxSize: '', + azureBackupRedundancyLevel: '', + azureServiceLevelObjective: '' + } + } : { + objectInfo: { + name: 'Database Properties1', + collationName: 'Latin1_General_100_CI_AS_KS_WS', + dateCreated: '5/31/2023 8:05:55 AM', + lastDatabaseBackup: 'None', + lastDatabaseLogBackup: 'None', + memoryAllocatedToMemoryOptimizedObjectsInMb: 0, + memoryUsedByMemoryOptimizedObjectsInMb: 0, + numberOfUsers: 5, + owner: 'databaseProperties 1', + sizeInMb: 16.00, + spaceAvailableInMb: 1.15, + status: 'Normal' + } + }; + } + private delayAndResolve(obj?: any): Promise { return new Promise((resolve, reject) => { setTimeout(() => { diff --git a/extensions/mssql/src/objectManagement/ui/databaseDialog.ts b/extensions/mssql/src/objectManagement/ui/databaseDialog.ts index bc01b70d43d3..86a7b6cab708 100644 --- a/extensions/mssql/src/objectManagement/ui/databaseDialog.ts +++ b/extensions/mssql/src/objectManagement/ui/databaseDialog.ts @@ -7,37 +7,84 @@ import * as azdata from 'azdata'; import { ObjectManagementDialogBase, ObjectManagementDialogOptions } from './objectManagementDialogBase'; import { IObjectManagementService } from 'mssql'; import * as localizedConstants from '../localizedConstants'; -import { CreateDatabaseDocUrl } from '../constants'; +import { CreateDatabaseDocUrl, DatabasePropertiesDocUrl } from '../constants'; import { Database, DatabaseViewInfo } from '../interfaces'; +import { convertNumToTwoDecimalStringinMB } from '../utils'; export class DatabaseDialog extends ObjectManagementDialogBase { - private _nameInput: azdata.InputBoxComponent; + // Database Properties tabs + private generalTab: azdata.Tab; + + //Database properties options + private nameInput: azdata.InputBoxComponent; + private backupSection: azdata.GroupContainer; + private lastDatabaseBackupInput: azdata.InputBoxComponent; + private lastDatabaseLogBackupInput: azdata.InputBoxComponent; + private databaseSection: azdata.GroupContainer; + private statusInput: azdata.InputBoxComponent; + private ownerInput: azdata.InputBoxComponent; + private dateCreatedInput: azdata.InputBoxComponent; + private sizeInput: azdata.InputBoxComponent; + private spaceAvailabeInput: azdata.InputBoxComponent; + private numberOfUsersInput: azdata.InputBoxComponent; + private memoryAllocatedInput: azdata.InputBoxComponent; + private memoryUsedInput: azdata.InputBoxComponent; + private collationInput: azdata.InputBoxComponent; constructor(objectManagementService: IObjectManagementService, options: ObjectManagementDialogOptions) { super(objectManagementService, options); } protected override get helpUrl(): string { - return CreateDatabaseDocUrl; + return this.options.isNewObject ? CreateDatabaseDocUrl : DatabasePropertiesDocUrl; } protected async initializeUI(): Promise { - let components = []; - components.push(this.initializeGeneralSection()); - components.push(this.initializeOptionsSection()); - if (this.viewInfo.isAzureDB) { - components.push(this.initializeConfigureSLOSection()); + if (this.options.isNewObject) { + let components = []; + components.push(this.initializeGeneralSection()); + components.push(this.initializeOptionsSection()); + if (this.viewInfo.isAzureDB) { + components.push(this.initializeConfigureSLOSection()); + } + this.formContainer.addItems(components); + } else { + // Initilaize general Tab sections + this.initializeBackupSection(); + this.initializeDatabaseSection(); + + // Initilaize general Tab + this.generalTab = { + title: localizedConstants.GeneralSectionHeader, + id: 'generalId', + content: this.createGroup('', [ + this.databaseSection, + this.backupSection + ], false) + }; + + // Initilaize tab group with tabbed panel + const propertiesTabGroup = { title: '', tabs: [this.generalTab] }; + const propertiesTabbedPannel = this.modelView.modelBuilder.tabbedPanel() + .withTabs([propertiesTabGroup]) + .withProps({ + CSSStyles: { + 'margin': '-10px 0px 0px -10px' + } + }) + .component(); + this.formContainer.addItem(propertiesTabbedPannel); } - this.formContainer.addItems(components); } + //#region Create Database private initializeGeneralSection(): azdata.GroupContainer { let containers: azdata.Component[] = []; - this._nameInput = this.createInputBox(localizedConstants.NameText, async () => { - this.objectInfo.name = this._nameInput.value; + this.nameInput = this.createInputBox(localizedConstants.NameText, async () => { + this.objectInfo.name = this.nameInput.value; await this.runValidation(false); }); - containers.push(this.createLabelInputContainer(localizedConstants.NameText, this._nameInput)); + containers.push(this.createLabelInputContainer(localizedConstants.NameText, this.nameInput)); if (this.viewInfo.loginNames?.length > 0) { this.objectInfo.owner = this.viewInfo.loginNames[0]; @@ -86,6 +133,67 @@ export class DatabaseDialog extends ObjectManagementDialogBase { }, this.objectInfo.lastDatabaseBackup, this.options.isNewObject); + const lastDatabaseBackupContainer = this.createLabelInputContainer(localizedConstants.LastDatabaseBackupText, this.lastDatabaseBackupInput); + + this.lastDatabaseLogBackupInput = this.createInputBox(localizedConstants.LastDatabaseLogBackupText, async () => { }, this.objectInfo.lastDatabaseLogBackup, this.options.isNewObject); + const lastDatabaseLogBackupContainer = this.createLabelInputContainer(localizedConstants.LastDatabaseLogBackupText, this.lastDatabaseLogBackupInput); + + this.backupSection = this.createGroup(localizedConstants.BackupSectionHeader, [ + lastDatabaseBackupContainer, + lastDatabaseLogBackupContainer + ], true); + } + + private initializeDatabaseSection(): void { + this.nameInput = this.createInputBox(localizedConstants.NamePropertyText, async () => { }, this.objectInfo.name, this.options.isNewObject); + const nameContainer = this.createLabelInputContainer(localizedConstants.NamePropertyText, this.nameInput); + + this.statusInput = this.createInputBox(localizedConstants.StatusText, async () => { }, this.objectInfo.status, this.options.isNewObject); + const statusContainer = this.createLabelInputContainer(localizedConstants.StatusText, this.statusInput); + + this.ownerInput = this.createInputBox(localizedConstants.OwnerPropertyText, async () => { }, this.objectInfo.owner, this.options.isNewObject); + const ownerContainer = this.createLabelInputContainer(localizedConstants.OwnerPropertyText, this.ownerInput); + + this.dateCreatedInput = this.createInputBox(localizedConstants.DateCreatedText, async () => { }, this.objectInfo.dateCreated, this.options.isNewObject); + const dateCreatedContainer = this.createLabelInputContainer(localizedConstants.DateCreatedText, this.dateCreatedInput); + + this.sizeInput = this.createInputBox(localizedConstants.SizeText, async () => { }, convertNumToTwoDecimalStringinMB(this.objectInfo.sizeInMb), this.options.isNewObject); + const sizeContainer = this.createLabelInputContainer(localizedConstants.SizeText, this.sizeInput); + + this.spaceAvailabeInput = this.createInputBox(localizedConstants.SpaceAvailableText, async () => { }, convertNumToTwoDecimalStringinMB(this.objectInfo.spaceAvailableInMb), this.options.isNewObject); + const spaceAvailabeContainer = this.createLabelInputContainer(localizedConstants.SpaceAvailableText, this.spaceAvailabeInput); + + this.numberOfUsersInput = this.createInputBox(localizedConstants.NumberOfUsersText, async () => { }, this.objectInfo.numberOfUsers.toString(), this.options.isNewObject); + const numberOfUsersContainer = this.createLabelInputContainer(localizedConstants.NumberOfUsersText, this.numberOfUsersInput); + + this.memoryAllocatedInput = this.createInputBox(localizedConstants.MemoryAllocatedText, async () => { }, convertNumToTwoDecimalStringinMB(this.objectInfo.memoryAllocatedToMemoryOptimizedObjectsInMb), this.options.isNewObject); + const memoryAllocatedContainer = this.createLabelInputContainer(localizedConstants.MemoryAllocatedText, this.memoryAllocatedInput); + + this.memoryUsedInput = this.createInputBox(localizedConstants.MemoryUsedText, async () => { }, convertNumToTwoDecimalStringinMB(this.objectInfo.memoryUsedByMemoryOptimizedObjectsInMb), this.options.isNewObject); + const memoryUsedContainer = this.createLabelInputContainer(localizedConstants.MemoryUsedText, this.memoryUsedInput); + + this.collationInput = this.createInputBox(localizedConstants.CollationText, async () => { }, this.objectInfo.collationName, this.options.isNewObject); + const collationContainer = this.createLabelInputContainer(localizedConstants.CollationText, this.collationInput); + + this.databaseSection = this.createGroup(localizedConstants.DatabaseSectionHeader, [ + nameContainer, + statusContainer, + ownerContainer, + collationContainer, + dateCreatedContainer, + sizeContainer, + spaceAvailabeContainer, + numberOfUsersContainer, + memoryAllocatedContainer, + memoryUsedContainer + ], true); + } + //#endregion private initializeConfigureSLOSection(): azdata.GroupContainer { let containers: azdata.Component[] = []; diff --git a/extensions/mssql/src/objectManagement/utils.ts b/extensions/mssql/src/objectManagement/utils.ts index 5dc721e0f114..9d509f11f503 100644 --- a/extensions/mssql/src/objectManagement/utils.ts +++ b/extensions/mssql/src/objectManagement/utils.ts @@ -42,3 +42,8 @@ export function isValidSQLPassword(password: string, userName: string = 'sa'): b const hasNonAlphas = /\W/.test(password) ? 1 : 0; return !containsUserName && password.length >= 8 && password.length <= 128 && (hasUpperCase + hasLowerCase + hasNumbers + hasNonAlphas >= 3); } + +// Converts number to two decimal placed string +export function convertNumToTwoDecimalStringinMB(value: number): string { + return localizedConstants.StringValueInMB(value?.toFixed(2)); +}