diff --git a/extensions/mssql/config.json b/extensions/mssql/config.json index af6a6be78f..a719c576d7 100644 --- a/extensions/mssql/config.json +++ b/extensions/mssql/config.json @@ -1,6 +1,6 @@ { "downloadUrl": "https://github.com/Microsoft/sqltoolsservice/releases/download/{#version#}/microsoft.sqltools.servicelayer-{#fileName#}", - "version": "4.7.0.21", + "version": "4.7.0.22", "downloadFileNames": { "Windows_86": "win-x86-net7.0.zip", "Windows_64": "win-x64-net7.0.zip", diff --git a/extensions/mssql/package.json b/extensions/mssql/package.json index e2110a5712..3bab915da4 100644 --- a/extensions/mssql/package.json +++ b/extensions/mssql/package.json @@ -494,23 +494,23 @@ }, { "command": "mssql.newObject", - "when": "connectionProvider == MSSQL && nodeType == Folder && objectType =~ /^(ServerLevelLogins|Users)$/ && config.workbench.enablePreviewFeatures", + "when": "connectionProvider == MSSQL && nodeType == Folder && objectType =~ /^(ServerLevelLogins|Users|ServerLevelServerRoles|ApplicationRoles|DatabaseRoles)$/ && config.workbench.enablePreviewFeatures", "group": "0_query@1" }, { "command": "mssql.objectProperties", - "when": "connectionProvider == MSSQL && nodeType =~ /^(ServerLevelLogin|User)$/ && config.workbench.enablePreviewFeatures", + "when": "connectionProvider == MSSQL && nodeType =~ /^(ServerLevelLogin|User|ServerLevelServerRole|ApplicationRole|DatabaseRole)$/ && config.workbench.enablePreviewFeatures", "group": "0_query@1", "isDefault": true }, { "command": "mssql.deleteObject", - "when": "connectionProvider == MSSQL && nodeType =~ /^(ServerLevelLogin|User)$/ && config.workbench.enablePreviewFeatures", + "when": "connectionProvider == MSSQL && nodeType =~ /^(ServerLevelLogin|User|ServerLevelServerRole|ApplicationRole|DatabaseRole)$/ && config.workbench.enablePreviewFeatures", "group": "0_query@2" }, { "command": "mssql.renameObject", - "when": "connectionProvider == MSSQL && nodeType =~ /^(Database|ServerLevelLogin|User|Table|View)$/ && config.workbench.enablePreviewFeatures", + "when": "connectionProvider == MSSQL && nodeType =~ /^(Database|ServerLevelLogin|User|Table|View|ServerLevelServerRole|ApplicationRole|DatabaseRole)$/ && config.workbench.enablePreviewFeatures", "group": "0_query@3" }, { @@ -540,22 +540,22 @@ }, { "command": "mssql.newObject", - "when": "connectionProvider == MSSQL && nodeType == Folder && objectType =~ /^(ServerLevelLogins|Users)$/ && config.workbench.enablePreviewFeatures", + "when": "connectionProvider == MSSQL && nodeType == Folder && objectType =~ /^(ServerLevelLogins|Users|ServerLevelServerRoles|ApplicationRoles|DatabaseRoles)$/ && config.workbench.enablePreviewFeatures", "group": "connection@1" }, { "command": "mssql.objectProperties", - "when": "connectionProvider == MSSQL && nodeType =~ /^(ServerLevelLogin|User)$/ && config.workbench.enablePreviewFeatures", + "when": "connectionProvider == MSSQL && nodeType =~ /^(ServerLevelLogin|User|ServerLevelServerRole|ApplicationRole|DatabaseRole)$/ && config.workbench.enablePreviewFeatures", "group": "connection@1" }, { "command": "mssql.deleteObject", - "when": "connectionProvider == MSSQL && nodeType =~ /^(ServerLevelLogin|User)$/ && config.workbench.enablePreviewFeatures", + "when": "connectionProvider == MSSQL && nodeType =~ /^(ServerLevelLogin|User|ServerLevelServerRole|ApplicationRole|DatabaseRole)$/ && config.workbench.enablePreviewFeatures", "group": "connection@2" }, { "command": "mssql.renameObject", - "when": "connectionProvider == MSSQL && nodeType =~ /^(Database|ServerLevelLogin|User|Table|View)$/ && config.workbench.enablePreviewFeatures", + "when": "connectionProvider == MSSQL && nodeType =~ /^(Database|ServerLevelLogin|User|Table|View|ServerLevelServerRole|ApplicationRole|DatabaseRole)$/ && config.workbench.enablePreviewFeatures", "group": "connection@3" }, { diff --git a/extensions/mssql/src/contracts.ts b/extensions/mssql/src/contracts.ts index 060774a9ea..d983a09e7e 100644 --- a/extensions/mssql/src/contracts.ts +++ b/extensions/mssql/src/contracts.ts @@ -1612,6 +1612,17 @@ export namespace DropObjectRequest { export const type = new RequestType('objectManagement/drop'); } +export interface SearchObjectRequestParams { + contextId: string; + searchText: string | undefined; + schema: string | undefined; + objectTypes: mssql.ObjectManagement.NodeType[]; +} + +export namespace SearchObjectRequest { + export const type = new RequestType('objectManagement/search'); +} + // ------------------------------- < Object Management > ------------------------------------ // ------------------------------- < Encryption IV/KEY updation Event > ------------------------------------ diff --git a/extensions/mssql/src/mssql.d.ts b/extensions/mssql/src/mssql.d.ts index 5e90658bfa..43ed493bc0 100644 --- a/extensions/mssql/src/mssql.d.ts +++ b/extensions/mssql/src/mssql.d.ts @@ -895,9 +895,12 @@ declare module 'mssql' { * Object types. */ export const enum NodeType { + ApplicationRole = "ApplicationRole", Column = "Column", Database = "Database", + DatabaseRole = "DatabaseRole", ServerLevelLogin = "ServerLevelLogin", + ServerLevelServerRole = "ServerLevelServerRole", Table = "Table", User = "User", View = "View" @@ -1212,6 +1215,112 @@ declare module 'mssql' { */ databaseRoles: string[]; } + + /** + * Interface representing the server role object. + */ + export interface ServerRoleInfo extends SqlObject { + /** + * Name of the server principal that owns the server role. + */ + owner: string; + /** + * Name of the server principals that are members of the server role. + */ + members: string[]; + /** + * Server roles that the server role is a member of. + */ + memberships: string[]; + } + + /** + * Interface representing the information required to render the server role view. + */ + export interface ServerRoleViewInfo extends ObjectViewInfo { + /** + * Whether the server role is a fixed role. + */ + isFixedRole: boolean; + /** + * List of all the server roles. + */ + serverRoles: string[]; + } + + /** + * Interface representing the application role object. + */ + export interface ApplicationRoleInfo extends SqlObject { + /** + * Default schema of the application role. + */ + defaultSchema: string; + /** + * Schemas owned by the application role. + */ + ownedSchemas: string[]; + /** + * Password of the application role. + */ + password: string; + } + + /** + * Interface representing the information required to render the application role view. + */ + export interface ApplicationRoleViewInfo extends ObjectViewInfo { + /** + * List of all the schemas in the database. + */ + schemas: string[]; + } + + /** + * Interface representing the database role object. + */ + export interface DatabaseRoleInfo extends SqlObject { + /** + * Name of the database principal that owns the database role. + */ + owner: string; + /** + * Schemas owned by the database role. + */ + ownedSchemas: string[]; + /** + * Name of the user or database role that are members of the database role. + */ + members: string[]; + } + + /** + * Interface representing the information required to render the database role view. + */ + export interface DatabaseRoleViewInfo extends ObjectViewInfo { + /** + * List of all the schemas in the database. + */ + schemas: string[]; + } + + /** + * Interface representing an item in the search result. + */ + export interface SearchResultItem { + /** + * name of the object. + */ + name: string; + /** + * type of the object. + */ + type: NodeType; + /** + * schema of the object. + */ + schema: string | undefined; + } } export interface IObjectManagementService { @@ -1258,6 +1367,14 @@ declare module 'mssql' { * @param objectUrn SMO Urn of the object to be dropped. More information: https://learn.microsoft.com/sql/relational-databases/server-management-objects-smo/overview-smo */ drop(connectionUri: string, objectType: ObjectManagement.NodeType, objectUrn: string): Thenable; + /** + * Search for objects. + * @param contextId The object view's context id. + * @param objectTypes The object types to search for. + * @param searchText Search text. + * @param schema Schema to search in. + */ + search(contextId: string, objectTypes: ObjectManagement.NodeType[], searchText?: string, schema?: string): Thenable; } // Object Management - End. } diff --git a/extensions/mssql/src/objectManagement/commands.ts b/extensions/mssql/src/objectManagement/commands.ts index aaedd48eee..dcd992d07c 100644 --- a/extensions/mssql/src/objectManagement/commands.ts +++ b/extensions/mssql/src/objectManagement/commands.ts @@ -17,6 +17,9 @@ import * as constants from '../constants'; import { getNodeTypeDisplayName, refreshParentNode } from './utils'; import { TelemetryReporter } from '../telemetry'; import { ObjectManagementDialogBase, ObjectManagementDialogOptions } from './ui/objectManagementDialogBase'; +import { ServerRoleDialog } from './ui/serverRoleDialog'; +import { DatabaseRoleDialog } from './ui/databaseRoleDialog'; +import { ApplicationRoleDialog } from './ui/applicationRoleDialog'; export function registerObjectManagementCommands(appContext: AppContext) { // Notes: Change the second parameter to false to use the actual object management service. @@ -48,13 +51,22 @@ async function handleNewObjectDialogCommand(context: azdata.ObjectExplorerContex if (!connectionUri) { return; } - let newObjectType: ObjectManagement.NodeType; + let objectType: ObjectManagement.NodeType; switch (context.nodeInfo!.objectType) { + case FolderType.ApplicationRoles: + objectType = ObjectManagement.NodeType.ApplicationRole; + break; + case FolderType.DatabaseRoles: + objectType = ObjectManagement.NodeType.DatabaseRole; + break; case FolderType.ServerLevelLogins: - newObjectType = ObjectManagement.NodeType.ServerLevelLogin; + objectType = ObjectManagement.NodeType.ServerLevelLogin; + break; + case FolderType.ServerLevelServerRoles: + objectType = ObjectManagement.NodeType.ServerLevelServerRole; break; case FolderType.Users: - newObjectType = ObjectManagement.NodeType.User; + objectType = ObjectManagement.NodeType.User; break; default: throw new Error(`Unsupported folder type: ${context.nodeInfo!.objectType}`); @@ -66,7 +78,7 @@ async function handleNewObjectDialogCommand(context: azdata.ObjectExplorerContex connectionUri: connectionUri, isNewObject: true, database: context.connectionProfile!.databaseName!, - objectType: newObjectType, + objectType: objectType, objectName: '', parentUrn: parentUrn, objectExplorerContext: context @@ -78,7 +90,8 @@ async function handleNewObjectDialogCommand(context: azdata.ObjectExplorerContex TelemetryReporter.createErrorEvent2(ObjectManagementViewName, TelemetryActions.OpenNewObjectDialog, err).withAdditionalProperties({ objectType: context.nodeInfo!.nodeType }).send(); - await vscode.window.showErrorMessage(localizedConstants.OpenNewObjectDialogError(localizedConstants.LoginTypeDisplayName, getErrorMessage(err))); + console.error(err); + await vscode.window.showErrorMessage(localizedConstants.OpenNewObjectDialogError(getNodeTypeDisplayName(objectType), getErrorMessage(err))); } } @@ -87,7 +100,6 @@ async function handleObjectPropertiesDialogCommand(context: azdata.ObjectExplore if (!connectionUri) { return; } - const nodeTypeDisplayName = getNodeTypeDisplayName(context.nodeInfo!.nodeType); try { const parentUrn = await getParentUrn(context); const options: ObjectManagementDialogOptions = { @@ -107,7 +119,8 @@ async function handleObjectPropertiesDialogCommand(context: azdata.ObjectExplore TelemetryReporter.createErrorEvent2(ObjectManagementViewName, TelemetryActions.OpenPropertiesDialog, err).withAdditionalProperties({ objectType: context.nodeInfo!.nodeType }).send(); - await vscode.window.showErrorMessage(localizedConstants.OpenObjectPropertiesDialogError(nodeTypeDisplayName, context.nodeInfo!.label, getErrorMessage(err))); + console.error(err); + await vscode.window.showErrorMessage(localizedConstants.OpenObjectPropertiesDialogError(getNodeTypeDisplayName(context.nodeInfo!.nodeType), context.nodeInfo!.label, getErrorMessage(err))); } } @@ -152,6 +165,7 @@ async function handleDeleteObjectCommand(context: azdata.ObjectExplorerContext, TelemetryReporter.createErrorEvent2(ObjectManagementViewName, TelemetryActions.DeleteObject, err).withAdditionalProperties({ objectType: context.nodeInfo!.nodeType }).send(); + console.error(err); return; } operation.updateStatus(azdata.TaskStatus.Succeeded); @@ -204,6 +218,7 @@ async function handleRenameObjectCommand(context: azdata.ObjectExplorerContext, TelemetryReporter.createErrorEvent2(ObjectManagementViewName, TelemetryActions.RenameObject, err).withAdditionalProperties({ objectType: context.nodeInfo!.nodeType }).send(); + console.error(err); return; } operation.updateStatus(azdata.TaskStatus.Succeeded); @@ -214,8 +229,14 @@ async function handleRenameObjectCommand(context: azdata.ObjectExplorerContext, function getDialog(service: IObjectManagementService, dialogOptions: ObjectManagementDialogOptions): ObjectManagementDialogBase> { switch (dialogOptions.objectType) { + case ObjectManagement.NodeType.ApplicationRole: + return new ApplicationRoleDialog(service, dialogOptions); + case ObjectManagement.NodeType.DatabaseRole: + return new DatabaseRoleDialog(service, dialogOptions); case ObjectManagement.NodeType.ServerLevelLogin: return new LoginDialog(service, dialogOptions); + case ObjectManagement.NodeType.ServerLevelServerRole: + return new ServerRoleDialog(service, dialogOptions); case ObjectManagement.NodeType.User: return new UserDialog(service, dialogOptions); default: diff --git a/extensions/mssql/src/objectManagement/constants.ts b/extensions/mssql/src/objectManagement/constants.ts index e41c2037ee..ff412f5e22 100644 --- a/extensions/mssql/src/objectManagement/constants.ts +++ b/extensions/mssql/src/objectManagement/constants.ts @@ -7,16 +7,25 @@ * The folder types in object explorer. */ export const enum FolderType { + ApplicationRoles = 'ApplicationRoles', + DatabaseRoles = 'DatabaseRoles', ServerLevelLogins = 'ServerLevelLogins', + ServerLevelServerRoles = 'ServerLevelServerRoles', Users = 'Users' } export const PublicServerRoleName = 'public'; -export const CreateUserDocUrl = 'https://learn.microsoft.com/en-us/sql/t-sql/statements/create-user-transact-sql'; -export const AlterUserDocUrl = 'https://learn.microsoft.com/en-us/sql/t-sql/statements/alter-user-transact-sql'; -export const CreateLoginDocUrl = 'https://learn.microsoft.com/en-us/sql/t-sql/statements/create-login-transact-sql'; -export const AlterLoginDocUrl = 'https://learn.microsoft.com/en-us/sql/t-sql/statements/alter-login-transact-sql'; +export const CreateUserDocUrl = 'https://learn.microsoft.com/sql/t-sql/statements/create-user-transact-sql'; +export const AlterUserDocUrl = 'https://learn.microsoft.com/sql/t-sql/statements/alter-user-transact-sql'; +export const CreateLoginDocUrl = 'https://learn.microsoft.com/sql/t-sql/statements/create-login-transact-sql'; +export const AlterLoginDocUrl = 'https://learn.microsoft.com/sql/t-sql/statements/alter-login-transact-sql'; +export const CreateServerRoleDocUrl = 'https://learn.microsoft.com/sql/t-sql/statements/create-server-role-transact-sql'; +export const AlterServerRoleDocUrl = 'https://learn.microsoft.com/sql/t-sql/statements/alter-server-role-transact-sql'; +export const CreateApplicationRoleDocUrl = 'https://learn.microsoft.com/sql/t-sql/statements/create-application-role-transact-sql'; +export const AlterApplicationRoleDocUrl = 'https://learn.microsoft.com/sql/t-sql/statements/alter-application-role-transact-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 enum TelemetryActions { CreateObject = 'CreateObject', diff --git a/extensions/mssql/src/objectManagement/localizedConstants.ts b/extensions/mssql/src/objectManagement/localizedConstants.ts index 4916d09fdc..f199fb4939 100644 --- a/extensions/mssql/src/objectManagement/localizedConstants.ts +++ b/extensions/mssql/src/objectManagement/localizedConstants.ts @@ -15,6 +15,12 @@ export const TableTypeDisplayName: string = localize('objectManagement.TableDisp export const ViewTypeDisplayName: string = localize('objectManagement.ViewDisplayName', "view"); export const ColumnTypeDisplayName: string = localize('objectManagement.ColumnDisplayName', "column"); export const DatabaseTypeDisplayName: string = localize('objectManagement.DatabaseDisplayName', "database"); +export const ServerRoleTypeDisplayName: string = localize('objectManagement.ServerRoleTypeDisplayName', "server role"); +export const ServerRoleTypeDisplayNameInTitle: string = localize('objectManagement.ServerRoleTypeDisplayNameInTitle', "Server Role"); +export const ApplicationRoleTypeDisplayName: string = localize('objectManagement.ApplicationRoleTypeDisplayName', "application role"); +export const ApplicationRoleTypeDisplayNameInTitle: string = localize('objectManagement.ApplicationRoleTypeDisplayNameInTitle', "Application Role"); +export const DatabaseRoleTypeDisplayName: string = localize('objectManagement.DatabaseRoleTypeDisplayName', "database role"); +export const DatabaseRoleTypeDisplayNameInTitle: string = localize('objectManagement.DatabaseRoleTypeDisplayNameInTitle', "Database Role"); // Shared Strings export const HelpText: string = localize('objectManagement.helpText', "Help"); @@ -26,6 +32,13 @@ export const RenameObjectDialogTitle: string = localize('objectManagement.rename export const ScriptText: string = localize('objectManagement.scriptText', "Script"); export const NoActionScriptedMessage: string = localize('objectManagement.noActionScriptedMessage', "There is no action to be scripted."); export const ScriptGeneratedText: string = localize('objectManagement.scriptGenerated', "Script has been generated successfully. You can close the dialog to view it in the newly opened editor.") +export const OwnerText: string = localize('objectManagement.ownerText', "Owner"); +export const BrowseText = localize('objectManagement.browseText', "Browse…"); +export const BrowseOwnerButtonAriaLabel = localize('objectManagement.browseForOwnerText', "Browse for an owner"); +export const AddText = localize('objectManagement.addText', "Add…"); +export const RemoveText = localize('objectManagement.removeText', "Remove"); +export const AddMemberAriaLabel = localize('objectManagement.addMemberText', "Add a member"); +export const RemoveMemberAriaLabel = localize('objectManagement.removeMemberText', "Remove selected member"); export function RefreshObjectExplorerError(error: string): string { @@ -128,6 +141,9 @@ export const PasswordCannotBeEmptyError = localize('objectManagement.passwordCan export const PasswordsNotMatchError = localize('objectManagement.passwordsNotMatchError', "Password must match the confirm password."); export const InvalidPasswordError = localize('objectManagement.invalidPasswordError', "Password doesn't meet the complexity requirement. For more information: https://docs.microsoft.com/sql/relational-databases/security/password-policy"); export const LoginNotSelectedError = localize('objectManagement.loginNotSelectedError', "Login is not selected."); +export const MembershipSectionHeader = localize('objectManagement.membershipLabel', "Membership"); +export const MemberSectionHeader = localize('objectManagement.membersLabel', "Members"); +export const SchemaText = localize('objectManagement.schemaLabel', "Schema"); // Login export const BlankPasswordConfirmationText: string = localize('objectManagement.blankPasswordConfirmation', "Creating a login with a blank password is a security risk. Are you sure you want to continue?"); @@ -158,4 +174,22 @@ export const UserWithNoConnectAccess = localize('objectManagement.user.userWithN export const DefaultSchemaText = localize('objectManagement.user.defaultSchemaLabel', "Default schema"); export const LoginText = localize('objectManagement.user.loginLabel', "Login"); export const OwnedSchemaSectionHeader = localize('objectManagement.user.ownedSchemasLabel', "Owned Schemas"); -export const MembershipSectionHeader = localize('objectManagement.user.membershipLabel', "Membership"); + +// Database Role +export const SelectDatabaseRoleMemberDialogTitle = localize('objectManagement.databaseRole.SelectMemberDialogTitle', "Select Database Role Members"); +export const SelectDatabaseRoleOwnerDialogTitle = localize('objectManagement.databaseRole.SelectOwnerDialogTitle', "Select Database Role Owner"); + +// Server Role +export const SelectServerRoleMemberDialogTitle = localize('objectManagement.serverRole.SelectMemberDialogTitle', "Select Server Role Members"); +export const SelectServerRoleOwnerDialogTitle = localize('objectManagement.serverRole.SelectOwnerDialogTitle', "Select Server Role Owner"); + +// Find Object Dialog +export const ObjectTypeText = localize('objectManagement.objectTypeLabel', "Object Type"); +export const FilterText = localize('objectManagement.filterText', "Filter"); +export const FindText = localize('objectManagement.findText', "Find"); +export const SelectText = localize('objectManagement.selectText', "Select"); +export const ObjectsText = localize('objectManagement.objectsLabel', "Objects"); +export const LoadingObjectsText = localize('objectManagement.loadingObjectsLabel', "Loading objects…"); +export function LoadingObjectsCompletedText(count: number): string { + return localize('objectManagement.loadingObjectsCompletedLabel', "Loading objects completed, {0} objects found", count); +} diff --git a/extensions/mssql/src/objectManagement/objectManagementService.ts b/extensions/mssql/src/objectManagement/objectManagementService.ts index 1415b1fa48..a15e891ca4 100644 --- a/extensions/mssql/src/objectManagement/objectManagementService.ts +++ b/extensions/mssql/src/objectManagement/objectManagementService.ts @@ -61,39 +61,39 @@ export class ObjectManagementService extends BaseService implements IObjectManag const params: contracts.DropObjectRequestParams = { connectionUri, objectUrn, objectType }; return this.runWithErrorHandling(contracts.DropObjectRequest.type, params); } + async search(contextId: string, objectTypes: ObjectManagement.NodeType[], searchText?: string, schema?: string): Promise { + const params: contracts.SearchObjectRequestParams = { contextId, searchText, objectTypes, schema }; + return this.runWithErrorHandling(contracts.SearchObjectRequest.type, params); + } } export class TestObjectManagementService implements IObjectManagementService { initializeView(contextId: string, objectType: ObjectManagement.NodeType, connectionUri: string, database: string, isNewObject: boolean, parentUrn: string, objectUrn: string): Thenable> { - if (objectType === ObjectManagement.NodeType.ServerLevelLogin) { - return Promise.resolve(this.getLoginView(isNewObject, objectUrn)); + let obj; + if (objectType === ObjectManagement.NodeType.ApplicationRole) { + obj = this.getApplicationRoleView(isNewObject, objectUrn); + } else if (objectType === ObjectManagement.NodeType.DatabaseRole) { + obj = this.getDatabaseRoleView(isNewObject, objectUrn); + } else if (objectType === ObjectManagement.NodeType.ServerLevelLogin) { + obj = this.getLoginView(isNewObject, objectUrn); + } else if (objectType === ObjectManagement.NodeType.ServerLevelServerRole) { + obj = this.getServerRoleView(isNewObject, objectUrn); } else if (objectType === ObjectManagement.NodeType.User) { - return Promise.resolve(this.getUserView(isNewObject, objectUrn)); + obj = this.getUserView(isNewObject, objectUrn); } else { throw Error('Not implemented'); } + return this.delayAndResolve(obj); } save(contextId: string, object: ObjectManagement.SqlObject): Thenable { - return new Promise((resolve, reject) => { - setTimeout(() => { - resolve(); - }, 3000); - }); + return this.delayAndResolve(); } script(contextId: string, object: ObjectManagement.SqlObject): Thenable { - return new Promise((resolve, reject) => { - setTimeout(() => { - resolve('test script'); - }, 1000); - }); + return this.delayAndResolve('test script'); } disposeView(contextId: string): Thenable { - return new Promise((resolve, reject) => { - setTimeout(() => { - resolve(); - }, 100); - }); + return this.delayAndResolve(); } async rename(connectionUri: string, objectType: ObjectManagement.NodeType, objectUrn: string, newName: string): Promise { return this.delayAndResolve(); @@ -101,6 +101,23 @@ export class TestObjectManagementService implements IObjectManagementService { async drop(connectionUri: string, objectType: ObjectManagement.NodeType, objectUrn: string): Promise { return this.delayAndResolve(); } + + async search(contextId: string, objectTypes: ObjectManagement.NodeType[], searchText: string, schema: string): Promise { + const items: ObjectManagement.SearchResultItem[] = []; + objectTypes.forEach(type => { + items.push(...this.generateSearchResult(type, 15)); + }); + return this.delayAndResolve(items); + } + + private generateSearchResult(objectType: ObjectManagement.NodeType, count: number): ObjectManagement.SearchResultItem[] { + let items: ObjectManagement.SearchResultItem[] = []; + for (let i = 0; i < count; i++) { + items.push({ name: `${objectType} ${i}`, type: objectType }); + } + return items; + } + private getLoginView(isNewObject: boolean, name: string): ObjectManagement.LoginViewInfo { const serverRoles = ['sysadmin', 'public', 'bulkadmin', 'dbcreator', 'diskadmin', 'processadmin', 'securityadmin', 'serveradmin']; const languages = ['', 'English']; @@ -160,6 +177,7 @@ export class TestObjectManagementService implements IObjectManagementService { } return login; } + private getUserView(isNewObject: boolean, name: string): ObjectManagement.UserViewInfo { let viewInfo: ObjectManagement.UserViewInfo; const languages = ['', 'English']; @@ -213,11 +231,73 @@ export class TestObjectManagementService implements IObjectManagementService { } return viewInfo; } - private delayAndResolve(): Promise { + + private getServerRoleView(isNewObject: boolean, name: string): ObjectManagement.ServerRoleViewInfo { + return isNewObject ? { + objectInfo: { + name: '', + members: [], + owner: '', + memberships: [] + }, + isFixedRole: false, + serverRoles: ['ServerLevelServerRole 1', 'ServerLevelServerRole 2', 'ServerLevelServerRole 3', 'ServerLevelServerRole 4'], + } : { + objectInfo: { + name: 'ServerLevelServerRole 1', + members: ['ServerLevelLogin 1', 'ServerLevelServerRole 2'], + owner: 'ServerLevelLogin 2', + memberships: ['ServerLevelServerRole 3', 'ServerLevelServerRole 4'] + }, + isFixedRole: false, + serverRoles: ['ServerLevelServerRole 2', 'ServerLevelServerRole 3', 'ServerLevelServerRole 4'] + }; + } + + private getApplicationRoleView(isNewObject: boolean, name: string): ObjectManagement.ApplicationRoleViewInfo { + return isNewObject ? { + objectInfo: { + name: '', + defaultSchema: 'dbo', + ownedSchemas: [], + }, + schemas: ['dbo', 'sys', 'admin'] + } : { + objectInfo: { + name: 'app role1', + password: '******************', + defaultSchema: 'dbo', + ownedSchemas: ['dbo'], + }, + schemas: ['dbo', 'sys', 'admin'] + }; + } + + private getDatabaseRoleView(isNewObject: boolean, name: string): ObjectManagement.DatabaseRoleViewInfo { + return isNewObject ? { + objectInfo: { + name: '', + owner: '', + members: [], + ownedSchemas: [] + }, + schemas: ['dbo', 'sys', 'admin'] + } : { + objectInfo: { + name: 'db role1', + owner: '', + members: [], + ownedSchemas: ['dbo'] + }, + schemas: ['dbo', 'sys', 'admin'] + }; + } + + private delayAndResolve(obj?: any): Promise { return new Promise((resolve, reject) => { setTimeout(() => { - resolve(); - }, 3000); + resolve(obj); + }, 1000); }); } } diff --git a/extensions/mssql/src/objectManagement/ui/applicationRoleDialog.ts b/extensions/mssql/src/objectManagement/ui/applicationRoleDialog.ts new file mode 100644 index 0000000000..a9b386454d --- /dev/null +++ b/extensions/mssql/src/objectManagement/ui/applicationRoleDialog.ts @@ -0,0 +1,94 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { ObjectManagementDialogBase, ObjectManagementDialogOptions } from './objectManagementDialogBase'; +import { IObjectManagementService, ObjectManagement } from 'mssql'; +import * as localizedConstants from '../localizedConstants'; +import { AlterApplicationRoleDocUrl, CreateApplicationRoleDocUrl } from '../constants'; +import { isValidSQLPassword } from '../utils'; +import { DefaultMaxTableHeight } from './dialogBase'; + +export class ApplicationRoleDialog extends ObjectManagementDialogBase { + // Sections + private generalSection: azdata.GroupContainer; + private ownedSchemasSection: azdata.GroupContainer; + + // General section content + private nameInput: azdata.InputBoxComponent; + private defaultSchemaDropdown: azdata.DropDownComponent; + private passwordInput: azdata.InputBoxComponent; + private confirmPasswordInput: azdata.InputBoxComponent; + + // Owned Schemas section content + private ownedSchemaTable: azdata.TableComponent; + + constructor(objectManagementService: IObjectManagementService, options: ObjectManagementDialogOptions) { + super(objectManagementService, options); + } + + protected override postInitializeData(): void { + this.objectInfo.password = this.objectInfo.password ?? ''; + } + + protected override get docUrl(): string { + return this.options.isNewObject ? CreateApplicationRoleDocUrl : AlterApplicationRoleDocUrl; + } + + protected override async validateInput(): Promise { + const errors = await super.validateInput(); + if (!this.objectInfo.password) { + errors.push(localizedConstants.PasswordCannotBeEmptyError); + } + if (this.objectInfo.password && !isValidSQLPassword(this.objectInfo.password, this.objectInfo.name) + && (this.options.isNewObject || this.objectInfo.password !== this.originalObjectInfo.password)) { + errors.push(localizedConstants.InvalidPasswordError); + } + if (this.objectInfo.password !== this.confirmPasswordInput.value) { + errors.push(localizedConstants.PasswordsNotMatchError); + } + return errors; + } + + protected async initializeUI(): Promise { + this.initializeGeneralSection(); + this.initializeOwnedSchemasSection(); + this.formContainer.addItems([this.generalSection, this.ownedSchemasSection]); + } + + private initializeGeneralSection(): void { + this.nameInput = this.createInputBox(localizedConstants.NameText, async (newValue) => { + this.objectInfo.name = newValue; + }, this.objectInfo.name, this.options.isNewObject); + const nameContainer = this.createLabelInputContainer(localizedConstants.NameText, this.nameInput); + + this.defaultSchemaDropdown = this.createDropdown(localizedConstants.DefaultSchemaText, async (newValue) => { + this.objectInfo.defaultSchema = newValue; + }, this.viewInfo.schemas, this.objectInfo.defaultSchema!); + const defaultSchemaContainer = this.createLabelInputContainer(localizedConstants.DefaultSchemaText, this.defaultSchemaDropdown); + + this.passwordInput = this.createPasswordInputBox(localizedConstants.PasswordText, async (newValue) => { + this.objectInfo.password = newValue; + }, this.objectInfo.password ?? ''); + const passwordContainer = this.createLabelInputContainer(localizedConstants.PasswordText, this.passwordInput); + + this.confirmPasswordInput = this.createPasswordInputBox(localizedConstants.ConfirmPasswordText, async () => { }, this.objectInfo.password ?? ''); + const confirmPasswordContainer = this.createLabelInputContainer(localizedConstants.ConfirmPasswordText, this.confirmPasswordInput); + + this.generalSection = this.createGroup(localizedConstants.GeneralSectionHeader, [nameContainer, defaultSchemaContainer, passwordContainer, confirmPasswordContainer], false); + } + + private initializeOwnedSchemasSection(): void { + this.ownedSchemaTable = this.createTableList(localizedConstants.OwnedSchemaSectionHeader, + [localizedConstants.SchemaText], + this.viewInfo.schemas, + this.objectInfo.ownedSchemas, + DefaultMaxTableHeight, + (item) => { + // It is not allowed to have unassigned schema. + return this.objectInfo.ownedSchemas.indexOf(item) === -1; + }); + this.ownedSchemasSection = this.createGroup(localizedConstants.OwnedSchemaSectionHeader, [this.ownedSchemaTable]); + } +} diff --git a/extensions/mssql/src/objectManagement/ui/databaseRoleDialog.ts b/extensions/mssql/src/objectManagement/ui/databaseRoleDialog.ts new file mode 100644 index 0000000000..5fe6cc531d --- /dev/null +++ b/extensions/mssql/src/objectManagement/ui/databaseRoleDialog.ts @@ -0,0 +1,130 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { ObjectManagementDialogBase, ObjectManagementDialogOptions } from './objectManagementDialogBase'; +import { IObjectManagementService, ObjectManagement } from 'mssql'; +import * as localizedConstants from '../localizedConstants'; +import { AlterDatabaseRoleDocUrl, CreateDatabaseRoleDocUrl } from '../constants'; +import { FindObjectDialog } from './findObjectDialog'; +import { DefaultMaxTableHeight } from './dialogBase'; + +export class DatabaseRoleDialog extends ObjectManagementDialogBase { + // Sections + private generalSection: azdata.GroupContainer; + private ownedSchemasSection: azdata.GroupContainer; + private memberSection: azdata.GroupContainer; + + // General section content + private nameInput: azdata.InputBoxComponent; + private ownerInput: azdata.InputBoxComponent; + + // Owned Schemas section content + private ownedSchemaTable: azdata.TableComponent; + + // Member section content + private memberTable: azdata.TableComponent; + + constructor(objectManagementService: IObjectManagementService, options: ObjectManagementDialogOptions) { + super(objectManagementService, options); + } + + protected override get docUrl(): string { + return this.options.isNewObject ? CreateDatabaseRoleDocUrl : AlterDatabaseRoleDocUrl; + } + + protected async initializeUI(): Promise { + this.initializeGeneralSection(); + this.initializeOwnedSchemasSection(); + this.initializeMemberSection(); + this.formContainer.addItems([this.generalSection, this.ownedSchemasSection, this.memberSection]); + } + + private initializeGeneralSection(): void { + this.nameInput = this.createInputBox(localizedConstants.NameText, async (newValue) => { + this.objectInfo.name = newValue; + }, this.objectInfo.name, this.options.isNewObject); + const nameContainer = this.createLabelInputContainer(localizedConstants.NameText, this.nameInput); + + this.ownerInput = this.createInputBox(localizedConstants.OwnerText, async (newValue) => { + this.objectInfo.owner = newValue; + }, this.objectInfo.owner, true, 'text', 210); + const browseOwnerButton = this.createButton(localizedConstants.BrowseText, localizedConstants.BrowseOwnerButtonAriaLabel, async () => { + const dialog = new FindObjectDialog(this.objectManagementService, { + objectTypes: [ObjectManagement.NodeType.ApplicationRole, ObjectManagement.NodeType.DatabaseRole, ObjectManagement.NodeType.User], + multiSelect: false, + contextId: this.contextId, + title: localizedConstants.SelectDatabaseRoleOwnerDialogTitle + }); + await dialog.open(); + const result = await dialog.waitForClose(); + if (result.selectedObjects.length > 0) { + this.ownerInput.value = result.selectedObjects[0].name; + } + }); + const ownerContainer = this.createLabelInputContainer(localizedConstants.OwnerText, this.ownerInput); + ownerContainer.addItems([browseOwnerButton], { flex: '0 0 auto' }); + + this.generalSection = this.createGroup(localizedConstants.GeneralSectionHeader, [nameContainer, ownerContainer], false); + } + + private initializeMemberSection(): void { + this.memberTable = this.createTable(localizedConstants.MemberSectionHeader, [ + { + type: azdata.ColumnType.text, + value: 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: [ObjectManagement.NodeType.DatabaseRole, ObjectManagement.NodeType.User], + multiSelect: true, + contextId: this.contextId, + title: localizedConstants.SelectDatabaseRoleMemberDialogTitle + }); + await dialog.open(); + const result = await dialog.waitForClose(); + this.addMembers(result.selectedObjects.map(r => r.name)); + }, + async () => { + if (this.memberTable.selectedRows.length === 1) { + this.removeMember(this.memberTable.selectedRows[0]); + } + }); + this.memberSection = this.createGroup(localizedConstants.MemberSectionHeader, [this.memberTable, buttonContainer]); + } + + private addMembers(names: string[]): void { + names.forEach(n => { + if (this.objectInfo.members.indexOf(n) === -1) { + this.objectInfo.members.push(n); + } + }); + this.updateMembersTable(); + } + + private removeMember(idx: number): void { + this.objectInfo.members.splice(idx, 1); + this.updateMembersTable(); + } + + private updateMembersTable(): void { + this.setTableData(this.memberTable, this.objectInfo.members.map(m => [m])); + this.onFormFieldChange(); + } + + private initializeOwnedSchemasSection(): void { + this.ownedSchemaTable = this.createTableList(localizedConstants.OwnedSchemaSectionHeader, + [localizedConstants.SchemaText], + this.viewInfo.schemas, + this.objectInfo.ownedSchemas, + DefaultMaxTableHeight, + (item) => { + // It is not allowed to have unassigned schema. + return this.objectInfo.ownedSchemas.indexOf(item) === -1; + }); + this.ownedSchemasSection = this.createGroup(localizedConstants.OwnedSchemaSectionHeader, [this.ownedSchemaTable]); + } +} diff --git a/extensions/mssql/src/objectManagement/ui/dialogBase.ts b/extensions/mssql/src/objectManagement/ui/dialogBase.ts new file mode 100644 index 0000000000..7a31df1f74 --- /dev/null +++ b/extensions/mssql/src/objectManagement/ui/dialogBase.ts @@ -0,0 +1,332 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { EOL } from 'os'; +import * as localizedConstants from '../localizedConstants'; + +export const DefaultLabelWidth = 150; +export const DefaultInputWidth = 300; +export const DefaultTableWidth = DefaultInputWidth + DefaultLabelWidth; +export const DefaultMaxTableHeight = 400; +export const DefaultMinTableRowCount = 1; +export const TableRowHeight = 25; +export const TableColumnHeaderHeight = 30; + +export function getTableHeight(rowCount: number, minRowCount: number = DefaultMinTableRowCount, maxHeight: number = DefaultMaxTableHeight): number { + return Math.min(Math.max(rowCount, minRowCount) * TableRowHeight + TableColumnHeaderHeight, maxHeight); +} + +export type TableListItemEnabledStateGetter = (item: T) => boolean; +export type TableListItemValueGetter = (item: T) => string[]; +export type TableListItemComparer = (item1: T, item2: T) => boolean; +export const DefaultTableListItemEnabledStateGetter: TableListItemEnabledStateGetter = (item: any) => true; +export const DefaultTableListItemValueGetter: TableListItemValueGetter = (item: any) => [item?.toString() ?? '']; +export const DefaultTableListItemComparer: TableListItemComparer = (item1: any, item2: any) => item1 === item2; + + +export abstract class DialogBase { + protected readonly disposables: vscode.Disposable[] = []; + protected readonly dialogObject: azdata.window.Dialog; + + private _modelView: azdata.ModelView; + private _loadingComponent: azdata.LoadingComponent; + private _formContainer: azdata.DivContainer; + private _closePromise: Promise; + + constructor(title: string, name: string, width: azdata.window.DialogWidth = 'narrow', style: azdata.window.DialogStyle = 'flyout') { + this.dialogObject = azdata.window.createModelViewDialog(title, name, width, style); + this.dialogObject.okButton.label = localizedConstants.OkText; + this.dialogObject.registerCloseValidator(async (): Promise => { + const confirmed = await this.onConfirmation(); + if (!confirmed) { + return false; + } + return await this.runValidation(); + }); + this._closePromise = new Promise(resolve => { + this.disposables.push(this.dialogObject.onClosed(async (reason: azdata.window.CloseReason) => { + await this.dispose(reason); + const result = reason === 'ok' ? this.dialogResult : undefined; + resolve(result); + })); + }); + } + + public waitForClose(): Promise { + return this._closePromise; + } + + protected get dialogResult(): DialogResult | undefined { return undefined; } + + protected async onConfirmation(): Promise { return true; } + + protected abstract initialize(): Promise; + + protected get formContainer(): azdata.DivContainer { return this._formContainer; } + + protected get modelView(): azdata.ModelView { return this._modelView; } + + protected onFormFieldChange(): void { } + + protected validateInput(): Promise { return Promise.resolve([]); } + + public async open(): Promise { + try { + this.onLoadingStatusChanged(true); + const initializeDialogPromise = new Promise((async resolve => { + await this.dialogObject.registerContent(async view => { + this._modelView = view; + this._formContainer = this.createFormContainer([]); + this._loadingComponent = view.modelBuilder.loadingComponent().withItem(this._formContainer).withProps({ + loading: true, + loadingText: localizedConstants.LoadingDialogText, + showText: true, + CSSStyles: { + width: "100%", + height: "100%" + } + }).component(); + await view.initializeModel(this._loadingComponent); + resolve(); + }); + })); + azdata.window.openDialog(this.dialogObject); + await initializeDialogPromise; + await this.initialize(); + this.onLoadingStatusChanged(false); + } catch (err) { + azdata.window.closeDialog(this.dialogObject); + throw err; + } + } + + protected async dispose(reason: azdata.window.CloseReason): Promise { + this.disposables.forEach(disposable => disposable.dispose()); + } + + protected async runValidation(showErrorMessage: boolean = true): Promise { + const errors = await this.validateInput(); + if (errors.length > 0 && (this.dialogObject.message?.text || showErrorMessage)) { + this.dialogObject.message = { + text: errors.join(EOL), + level: azdata.window.MessageLevel.Error + }; + } else { + this.dialogObject.message = undefined; + } + return errors.length === 0; + } + + protected createLabelInputContainer(label: string, input: azdata.InputBoxComponent | azdata.DropDownComponent): azdata.FlexContainer { + const labelComponent = this.modelView.modelBuilder.text().withProps({ width: DefaultLabelWidth, value: label, requiredIndicator: input.required }).component(); + const container = this.modelView.modelBuilder.flexContainer().withLayout({ flexFlow: 'horizontal', flexWrap: 'nowrap', alignItems: 'center' }).withItems([labelComponent], { flex: '0 0 auto' }).component(); + container.addItem(input, { flex: '1 1 auto' }); + return container; + } + + protected createCheckbox(label: string, handler: (checked: boolean) => Promise, checked: boolean = false, enabled: boolean = true): azdata.CheckBoxComponent { + const checkbox = this.modelView.modelBuilder.checkBox().withProps({ + label: label, + checked: checked, + enabled: enabled + }).component(); + this.disposables.push(checkbox.onChanged(async () => { + await handler(checkbox.checked!); + this.onFormFieldChange(); + await this.runValidation(false); + })); + return checkbox; + } + + protected createPasswordInputBox(ariaLabel: string, textChangeHandler: (newValue: string) => Promise, value: string = '', enabled: boolean = true, width: number = DefaultInputWidth): azdata.InputBoxComponent { + return this.createInputBox(ariaLabel, textChangeHandler, value, enabled, 'password', width); + } + + protected createInputBox(ariaLabel: string, textChangeHandler: (newValue: string) => Promise, value: string = '', enabled: boolean = true, type: azdata.InputBoxInputType = 'text', width: number = DefaultInputWidth): azdata.InputBoxComponent { + const inputbox = this.modelView.modelBuilder.inputBox().withProps({ inputType: type, enabled: enabled, ariaLabel: ariaLabel, value: value, width: width }).component(); + this.disposables.push(inputbox.onTextChanged(async () => { + await textChangeHandler(inputbox.value!); + this.onFormFieldChange(); + await this.runValidation(false); + })); + return inputbox; + } + + protected createGroup(header: string, items: azdata.Component[], collapsible: boolean = true, collapsed: boolean = false): azdata.GroupContainer { + return this.modelView.modelBuilder.groupContainer().withLayout({ + header: header, + collapsible: collapsible, + collapsed: collapsed + }).withItems(items).component(); + } + + protected createTableList(ariaLabel: string, + columnNames: string[], + allItems: T[], + selectedItems: T[], + maxHeight: number = DefaultMaxTableHeight, + enabledStateGetter: TableListItemEnabledStateGetter = DefaultTableListItemEnabledStateGetter, + rowValueGetter: TableListItemValueGetter = DefaultTableListItemValueGetter, + itemComparer: TableListItemComparer = DefaultTableListItemComparer): azdata.TableComponent { + const data = this.getDataForTableList(allItems, selectedItems, enabledStateGetter, rowValueGetter, itemComparer); + const table = this.modelView.modelBuilder.table().withProps( + { + ariaLabel: ariaLabel, + data: data, + columns: [ + { + value: localizedConstants.SelectedText, + type: azdata.ColumnType.checkBox, + options: { actionOnCheckbox: azdata.ActionOnCellCheckboxCheck.customAction } + }, ...columnNames.map(name => { + return { value: name }; + }) + ], + width: DefaultTableWidth, + height: getTableHeight(data.length, DefaultMinTableRowCount, maxHeight) + } + ).component(); + this.disposables.push(table.onCellAction!((arg: azdata.ICheckboxCellActionEventArgs) => { + const item = allItems[arg.row]; + const idx = selectedItems.findIndex(i => itemComparer(i, item)); + if (arg.checked && idx === -1) { + selectedItems.push(item); + } else if (!arg.checked && idx !== -1) { + selectedItems.splice(idx, 1) + } + this.onFormFieldChange(); + })); + return table; + } + + protected setTableData(table: azdata.TableComponent, data: any[][], maxHeight: number = DefaultMaxTableHeight) { + table.data = data; + table.height = getTableHeight(data.length, DefaultMinTableRowCount, maxHeight); + } + + protected getDataForTableList( + allItems: T[], + selectedItems: T[], + enabledStateGetter: TableListItemEnabledStateGetter = DefaultTableListItemEnabledStateGetter, + rowValueGetter: TableListItemValueGetter = DefaultTableListItemValueGetter, + itemComparer: TableListItemComparer = DefaultTableListItemComparer): any[][] { + return allItems.map(item => { + const idx = selectedItems.findIndex(i => itemComparer(i, item)); + const stateColumnValue = { checked: idx !== -1, enabled: enabledStateGetter(item) }; + return [stateColumnValue, ...rowValueGetter(item)]; + }); + } + + protected createTable(ariaLabel: string, columns: azdata.TableColumn[], data: any[][], maxHeight: number = DefaultMaxTableHeight): azdata.TableComponent { + const table = this.modelView.modelBuilder.table().withProps( + { + ariaLabel: ariaLabel, + data: data, + columns: columns, + width: DefaultTableWidth, + height: getTableHeight(data.length, DefaultMinTableRowCount, maxHeight) + } + ).component(); + return table; + } + + protected addButtonsForTable(table: azdata.TableComponent, addButtonAriaLabel: string, removeButtonAriaLabel: string, addHandler: () => Promise, removeHandler: () => void): azdata.FlexContainer { + let addButton: azdata.ButtonComponent; + let removeButton: azdata.ButtonComponent; + const updateButtons = () => { + removeButton.enabled = table.selectedRows.length > 0; + } + addButton = this.createButton(localizedConstants.AddText, addButtonAriaLabel, async () => { + await addHandler(); + updateButtons(); + }); + removeButton = this.createButton(localizedConstants.RemoveText, removeButtonAriaLabel, async () => { + await removeHandler(); + updateButtons(); + }, false); + this.disposables.push(table.onRowSelected(() => { + updateButtons(); + })); + return this.createButtonContainer([addButton, removeButton]); + } + + protected createDropdown(ariaLabel: string, handler: (newValue: string) => Promise, values: string[], value: string | undefined, enabled: boolean = true, width: number = DefaultInputWidth): azdata.DropDownComponent { + // Automatically add an empty item to the beginning of the list if the current value is not specified. + // This is needed when no meaningful default value can be provided. + // Create a new array so that the original array isn't modified. + const dropdownValues = []; + dropdownValues.push(...values); + if (!value) { + dropdownValues.unshift(''); + } + const dropdown = this.modelView.modelBuilder.dropDown().withProps({ + ariaLabel: ariaLabel, + values: dropdownValues, + value: value, + width: width, + enabled: enabled + }).component(); + this.disposables.push(dropdown.onValueChanged(async () => { + await handler(dropdown.value!); + this.onFormFieldChange(); + await this.runValidation(false); + })); + return dropdown; + } + + protected createButton(label: string, ariaLabel: string, handler: () => Promise, enabled: boolean = true): azdata.ButtonComponent { + const button = this.modelView.modelBuilder.button().withProps({ + label: label, + ariaLabel: ariaLabel, + enabled: enabled, + secondary: true, + CSSStyles: { 'min-width': '70px', 'margin-left': '5px' } + }).component(); + this.disposables.push(button.onDidClick(async () => { + await handler(); + })); + return button; + } + + protected createButtonContainer(items: azdata.ButtonComponent[], justifyContent: azdata.JustifyContentType = 'flex-end'): azdata.FlexContainer { + return this.modelView.modelBuilder.flexContainer().withProps({ + CSSStyles: { 'margin': '5px 0' } + }).withLayout({ + flexFlow: 'horizontal', + flexWrap: 'nowrap', + justifyContent: justifyContent + }).withItems(items, { flex: '0 0 auto' }).component(); + } + + protected removeItem(container: azdata.DivContainer | azdata.FlexContainer, item: azdata.Component): void { + if (container.items.indexOf(item) !== -1) { + container.removeItem(item); + } + } + + protected addItem(container: azdata.DivContainer | azdata.FlexContainer, item: azdata.Component, index?: number): void { + if (container.items.indexOf(item) === -1) { + if (index === undefined) { + container.addItem(item); + } else { + container.insertItem(item, index); + } + } + } + + protected onLoadingStatusChanged(isLoading: boolean): void { + if (this._loadingComponent) { + this._loadingComponent.loading = isLoading; + } + } + + private createFormContainer(items: azdata.Component[]): azdata.DivContainer { + return this.modelView.modelBuilder.divContainer().withLayout({ width: 'calc(100% - 20px)', height: 'calc(100% - 20px)' }).withProps({ + CSSStyles: { 'padding': '10px' } + }).withItems(items, { CSSStyles: { 'margin-block-end': '10px' } }).component(); + } +} diff --git a/extensions/mssql/src/objectManagement/ui/findObjectDialog.ts b/extensions/mssql/src/objectManagement/ui/findObjectDialog.ts new file mode 100644 index 0000000000..e8158cf5ca --- /dev/null +++ b/extensions/mssql/src/objectManagement/ui/findObjectDialog.ts @@ -0,0 +1,136 @@ +/*--------------------------------------------------------------------------------------------- + * 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 mssql from 'mssql'; +import { DefaultTableListItemEnabledStateGetter, DefaultMaxTableHeight, DialogBase, TableListItemComparer, TableListItemValueGetter } from './dialogBase'; +import * as localizedConstants from '../localizedConstants'; +import { getNodeTypeDisplayName } from '../utils'; +import { getErrorMessage } from '../../utils'; + +export interface FindObjectDialogOptions { + objectTypes: mssql.ObjectManagement.NodeType[]; + multiSelect: boolean; + contextId: string; + title: string; +} + +export interface FindObjectDialogResult { + selectedObjects: mssql.ObjectManagement.SearchResultItem[]; +} + +const ObjectComparer: TableListItemComparer = + (item1, item2) => { + return item1.name === item2.name && item1.type === item2.type; + }; + +const ObjectRowValueGetter: TableListItemValueGetter = + (item) => { + return [item.name, getNodeTypeDisplayName(item.type, true)]; + }; + +const ObjectsTableMaxHeight = 700; + +export class FindObjectDialog extends DialogBase { + private objectTypesTable: azdata.TableComponent; + private findButton: azdata.ButtonComponent; + private objectsTable: azdata.TableComponent; + private objectsLoadingComponent: azdata.LoadingComponent; + private result: FindObjectDialogResult; + private selectedObjectTypes: string[] = []; + private allObjects: mssql.ObjectManagement.SearchResultItem[] = []; + + constructor(private readonly objectManagementService: mssql.IObjectManagementService, private readonly options: FindObjectDialogOptions) { + super(options.title, 'FindObjectDialog'); + this.dialogObject.okButton.label = localizedConstants.SelectText; + this.result = { + selectedObjects: [] + }; + this.selectedObjectTypes = [...options.objectTypes]; + } + + protected override async initialize(): Promise { + this.dialogObject.okButton.enabled = false; + this.objectTypesTable = this.createTableList(localizedConstants.ObjectTypeText, + [localizedConstants.ObjectTypeText], + this.options.objectTypes, + this.selectedObjectTypes, + DefaultMaxTableHeight, + DefaultTableListItemEnabledStateGetter, (item) => { + return [getNodeTypeDisplayName(item, true)]; + }); + this.findButton = this.createButton(localizedConstants.FindText, localizedConstants.FindText, async () => { + await this.onFindObjectButtonClick(); + }); + const buttonContainer = this.createButtonContainer([this.findButton]); + const objectTypeSection = this.createGroup(localizedConstants.ObjectTypeText, [this.objectTypesTable, buttonContainer]); + + if (this.options.multiSelect) { + this.objectsTable = this.createTableList(localizedConstants.ObjectsText, + [localizedConstants.NameText, localizedConstants.ObjectTypeText], + this.allObjects, + this.result.selectedObjects, + ObjectsTableMaxHeight, + DefaultTableListItemEnabledStateGetter, + ObjectRowValueGetter, + ObjectComparer); + } else { + this.objectsTable = this.createTable(localizedConstants.ObjectsText, [{ + value: localizedConstants.NameText, + }, { + value: localizedConstants.ObjectTypeText + }], []); + this.disposables.push(this.objectsTable.onRowSelected(async () => { + if (this.objectsTable.selectedRows.length > 0) { + this.result.selectedObjects = [this.allObjects[this.objectsTable.selectedRows[0]]]; + } + await this.onFormFieldChange(); + })); + } + this.objectsLoadingComponent = this.modelView.modelBuilder.loadingComponent().withItem(this.objectsTable).withProps({ + loadingText: localizedConstants.LoadingObjectsText, + showText: true, + loading: false + }).component(); + const objectsSection = this.createGroup(localizedConstants.ObjectsText, [this.objectsLoadingComponent]); + + this.formContainer.addItems([objectTypeSection, objectsSection]); + } + + protected override get dialogResult(): FindObjectDialogResult | undefined { + return this.result; + } + + private async onFindObjectButtonClick(): Promise { + this.dialogObject.okButton.enabled = false; + this.objectsLoadingComponent.loading = true; + this.findButton.enabled = false; + try { + const results = await this.objectManagementService.search(this.options.contextId, this.selectedObjectTypes); + this.allObjects.splice(0, this.allObjects.length, ...results); + let data; + if (this.options.multiSelect) { + data = this.getDataForTableList(this.allObjects, this.result.selectedObjects, DefaultTableListItemEnabledStateGetter, ObjectRowValueGetter, ObjectComparer); + } + else { + data = this.allObjects.map(item => ObjectRowValueGetter(item)); + } + this.setTableData(this.objectsTable, data, ObjectsTableMaxHeight); + this.objectsLoadingComponent.loadingCompletedText = localizedConstants.LoadingObjectsCompletedText(results.length); + } catch (err) { + this.dialogObject.message = { + text: getErrorMessage(err), + level: azdata.window.MessageLevel.Error + }; + } + this.findButton.enabled = true; + this.objectsLoadingComponent.loading = false; + } + + protected override async onFormFieldChange(): Promise { + this.findButton.enabled = this.selectedObjectTypes.length > 0; + this.dialogObject.okButton.enabled = this.result.selectedObjects.length > 0; + } +} diff --git a/extensions/mssql/src/objectManagement/ui/loginDialog.ts b/extensions/mssql/src/objectManagement/ui/loginDialog.ts index 0fbefc3443..2269f845ac 100644 --- a/extensions/mssql/src/objectManagement/ui/loginDialog.ts +++ b/extensions/mssql/src/objectManagement/ui/loginDialog.ts @@ -4,11 +4,12 @@ *--------------------------------------------------------------------------------------------*/ import * as azdata from 'azdata'; import * as vscode from 'vscode'; -import { DefaultInputWidth, ObjectManagementDialogBase, ObjectManagementDialogOptions } from './objectManagementDialogBase'; +import { ObjectManagementDialogBase, ObjectManagementDialogOptions } from './objectManagementDialogBase'; import { IObjectManagementService, ObjectManagement } from 'mssql'; import * as localizedConstants from '../localizedConstants'; import { AlterLoginDocUrl, CreateLoginDocUrl, PublicServerRoleName } from '../constants'; import { getAuthenticationTypeByDisplayName, getAuthenticationTypeDisplayName, isValidSQLPassword } from '../utils'; +import { DefaultMaxTableHeight } from './dialogBase'; export class LoginDialog extends ObjectManagementDialogBase { private generalSection: azdata.GroupContainer; @@ -52,11 +53,8 @@ export class LoginDialog extends ObjectManagementDialogBase { - const errors: string[] = []; - if (!this.objectInfo.name) { - errors.push(localizedConstants.NameCannotBeEmptyError); - } + protected override async validateInput(): Promise { + const errors = await super.validateInput(); if (this.objectInfo.authenticationType === ObjectManagement.AuthenticationType.Sql) { if (!this.objectInfo.password && !(this.viewInfo.supportAdvancedPasswordOptions && !this.objectInfo.enforcePasswordPolicy)) { errors.push(localizedConstants.PasswordCannotBeEmptyError); @@ -104,17 +102,9 @@ export class LoginDialog extends ObjectManagementDialogBase { - this.objectInfo.name = this.nameInput.value!; - this.onObjectValueChange(); - await this.runValidation(false); - })); + this.nameInput = this.createInputBox(localizedConstants.NameText, async (newValue) => { + this.objectInfo.name = newValue; + }, this.objectInfo.name, this.options.isNewObject); const nameContainer = this.createLabelInputContainer(localizedConstants.NameText, this.nameInput); const authTypes = []; @@ -127,93 +117,72 @@ export class LoginDialog extends ObjectManagementDialogBase { - this.objectInfo.authenticationType = getAuthenticationTypeByDisplayName(this.authTypeDropdown.value); + this.authTypeDropdown = this.createDropdown(localizedConstants.AuthTypeText, async (newValue) => { + this.objectInfo.authenticationType = getAuthenticationTypeByDisplayName(newValue); this.setViewByAuthenticationType(); - this.onObjectValueChange(); - await this.runValidation(false); - })); + }, authTypes, getAuthenticationTypeDisplayName(this.objectInfo.authenticationType), this.options.isNewObject); + const authTypeContainer = this.createLabelInputContainer(localizedConstants.AuthTypeText, this.authTypeDropdown); - this.enabledCheckbox = this.createCheckbox(localizedConstants.EnabledText, this.objectInfo.isEnabled); - this.disposables.push(this.enabledCheckbox.onChanged(() => { - this.objectInfo.isEnabled = this.enabledCheckbox.checked!; - this.onObjectValueChange(); - })); + this.enabledCheckbox = this.createCheckbox(localizedConstants.EnabledText, async (checked) => { + this.objectInfo.isEnabled = checked; + }, this.objectInfo.isEnabled); this.generalSection = this.createGroup(localizedConstants.GeneralSectionHeader, [nameContainer, authTypeContainer, this.enabledCheckbox], false); } private initializeSqlAuthSection(): void { const items: azdata.Component[] = []; - this.passwordInput = this.createPasswordInputBox(localizedConstants.PasswordText, this.objectInfo.password ?? ''); + this.passwordInput = this.createPasswordInputBox(localizedConstants.PasswordText, async (newValue) => { + this.objectInfo.password = newValue; + }, this.objectInfo.password ?? ''); const passwordRow = this.createLabelInputContainer(localizedConstants.PasswordText, this.passwordInput); - this.confirmPasswordInput = this.createPasswordInputBox(localizedConstants.ConfirmPasswordText, this.objectInfo.password ?? ''); - this.disposables.push(this.passwordInput.onTextChanged(async () => { - this.objectInfo.password = this.passwordInput.value; - this.onObjectValueChange(); - await this.runValidation(false); - })); - this.disposables.push(this.confirmPasswordInput.onTextChanged(async () => { - await this.runValidation(false); - })); + this.confirmPasswordInput = this.createPasswordInputBox(localizedConstants.ConfirmPasswordText, async () => { }, this.objectInfo.password ?? ''); const confirmPasswordRow = this.createLabelInputContainer(localizedConstants.ConfirmPasswordText, this.confirmPasswordInput); items.push(passwordRow, confirmPasswordRow); if (!this.options.isNewObject) { - this.specifyOldPasswordCheckbox = this.createCheckbox(localizedConstants.SpecifyOldPasswordText); - this.oldPasswordInput = this.createPasswordInputBox(localizedConstants.OldPasswordText, '', false); - const oldPasswordRow = this.createLabelInputContainer(localizedConstants.OldPasswordText, this.oldPasswordInput); - this.disposables.push(this.specifyOldPasswordCheckbox.onChanged(async () => { + this.specifyOldPasswordCheckbox = this.createCheckbox(localizedConstants.SpecifyOldPasswordText, async (checked) => { this.oldPasswordInput.enabled = this.specifyOldPasswordCheckbox.checked; this.objectInfo.oldPassword = ''; if (!this.specifyOldPasswordCheckbox.checked) { this.oldPasswordInput.value = ''; } - this.onObjectValueChange(); - await this.runValidation(false); - })); - this.disposables.push(this.oldPasswordInput.onTextChanged(async () => { - this.objectInfo.oldPassword = this.oldPasswordInput.value; - this.onObjectValueChange(); - await this.runValidation(false); - })); + }); + this.oldPasswordInput = this.createPasswordInputBox(localizedConstants.OldPasswordText, async (newValue) => { + this.objectInfo.oldPassword = newValue; + }, '', false); + const oldPasswordRow = this.createLabelInputContainer(localizedConstants.OldPasswordText, this.oldPasswordInput); items.push(this.specifyOldPasswordCheckbox, oldPasswordRow); } if (this.viewInfo.supportAdvancedPasswordOptions) { - this.enforcePasswordPolicyCheckbox = this.createCheckbox(localizedConstants.EnforcePasswordPolicyText, this.objectInfo.enforcePasswordPolicy); - this.enforcePasswordExpirationCheckbox = this.createCheckbox(localizedConstants.EnforcePasswordExpirationText, this.objectInfo.enforcePasswordPolicy); - this.mustChangePasswordCheckbox = this.createCheckbox(localizedConstants.MustChangePasswordText, this.objectInfo.mustChangePassword); - this.disposables.push(this.enforcePasswordPolicyCheckbox.onChanged(async () => { - const enforcePolicy = this.enforcePasswordPolicyCheckbox.checked; + this.enforcePasswordPolicyCheckbox = this.createCheckbox(localizedConstants.EnforcePasswordPolicyText, async (checked) => { + const enforcePolicy = checked; this.objectInfo.enforcePasswordPolicy = enforcePolicy; this.enforcePasswordExpirationCheckbox.enabled = enforcePolicy; this.mustChangePasswordCheckbox.enabled = enforcePolicy; this.enforcePasswordExpirationCheckbox.checked = enforcePolicy; this.mustChangePasswordCheckbox.checked = enforcePolicy; - this.onObjectValueChange(); - await this.runValidation(false); - })); - this.disposables.push(this.enforcePasswordExpirationCheckbox.onChanged(() => { - const enforceExpiration = this.enforcePasswordExpirationCheckbox.checked; + }, this.objectInfo.enforcePasswordPolicy); + + this.enforcePasswordExpirationCheckbox = this.createCheckbox(localizedConstants.EnforcePasswordExpirationText, async (checked) => { + const enforceExpiration = checked; this.objectInfo.enforcePasswordExpiration = enforceExpiration; this.mustChangePasswordCheckbox.enabled = enforceExpiration; this.mustChangePasswordCheckbox.checked = enforceExpiration; - this.onObjectValueChange(); - })); - this.disposables.push(this.mustChangePasswordCheckbox.onChanged(() => { - this.objectInfo.mustChangePassword = this.mustChangePasswordCheckbox.checked; - this.onObjectValueChange(); - })); + }, this.objectInfo.enforcePasswordPolicy); + + this.mustChangePasswordCheckbox = this.createCheckbox(localizedConstants.MustChangePasswordText, async (checked) => { + this.objectInfo.mustChangePassword = checked; + }, this.objectInfo.mustChangePassword); + items.push(this.enforcePasswordPolicyCheckbox, this.enforcePasswordExpirationCheckbox, this.mustChangePasswordCheckbox); + if (!this.options.isNewObject) { - this.lockedOutCheckbox = this.createCheckbox(localizedConstants.LoginLockedOutText, this.objectInfo.isLockedOut, this.viewInfo.canEditLockedOutState); + this.lockedOutCheckbox = this.createCheckbox(localizedConstants.LoginLockedOutText, async (checked) => { + this.objectInfo.isLockedOut = checked; + }, this.objectInfo.isLockedOut, this.viewInfo.canEditLockedOutState); items.push(this.lockedOutCheckbox); - this.disposables.push(this.lockedOutCheckbox.onChanged(() => { - this.objectInfo.isLockedOut = this.lockedOutCheckbox.checked!; - this.onObjectValueChange(); - })); } } @@ -223,25 +192,19 @@ export class LoginDialog extends ObjectManagementDialogBase { + this.objectInfo.defaultDatabase = newValue; + }, this.viewInfo.databases, this.objectInfo.defaultDatabase); const defaultDatabaseContainer = this.createLabelInputContainer(localizedConstants.DefaultDatabaseText, this.defaultDatabaseDropdown); - this.disposables.push(this.defaultDatabaseDropdown.onValueChanged(() => { - this.objectInfo.defaultDatabase = this.defaultDatabaseDropdown.value; - this.onObjectValueChange(); - })); - this.defaultLanguageDropdown = this.createDropdown(localizedConstants.DefaultLanguageText, this.viewInfo.languages, this.objectInfo.defaultLanguage); + this.defaultLanguageDropdown = this.createDropdown(localizedConstants.DefaultLanguageText, async (newValue) => { + this.objectInfo.defaultLanguage = newValue; + }, this.viewInfo.languages, this.objectInfo.defaultLanguage); const defaultLanguageContainer = this.createLabelInputContainer(localizedConstants.DefaultLanguageText, this.defaultLanguageDropdown); - this.disposables.push(this.defaultLanguageDropdown.onValueChanged(() => { - this.objectInfo.defaultLanguage = this.defaultLanguageDropdown.value; - this.onObjectValueChange(); - })); - this.connectPermissionCheckbox = this.createCheckbox(localizedConstants.PermissionToConnectText, this.objectInfo.connectPermission); - this.disposables.push(this.connectPermissionCheckbox.onChanged(() => { - this.objectInfo.connectPermission = this.connectPermissionCheckbox.checked!; - this.onObjectValueChange(); - })); + this.connectPermissionCheckbox = this.createCheckbox(localizedConstants.PermissionToConnectText, async (checked) => { + this.objectInfo.connectPermission = checked; + }, this.objectInfo.connectPermission); items.push(defaultDatabaseContainer, defaultLanguageContainer, this.connectPermissionCheckbox); } @@ -249,12 +212,14 @@ export class LoginDialog extends ObjectManagementDialogBase { - const isRoleSelected = this.objectInfo.serverRoles.indexOf(name) !== -1; - const isRoleSelectionEnabled = name !== PublicServerRoleName; - return [{ enabled: isRoleSelectionEnabled, checked: isRoleSelected }, name]; - }); - this.serverRoleTable = this.createTableList(localizedConstants.ServerRoleSectionHeader, this.viewInfo.serverRoles, this.objectInfo.serverRoles, serverRolesData); + this.serverRoleTable = this.createTableList(localizedConstants.ServerRoleSectionHeader, + [localizedConstants.ServerRoleTypeDisplayNameInTitle], + this.viewInfo.serverRoles, + this.objectInfo.serverRoles, + DefaultMaxTableHeight, + (item) => { + return item !== PublicServerRoleName + }); this.serverRoleSection = this.createGroup(localizedConstants.ServerRoleSectionHeader, [this.serverRoleTable]); } diff --git a/extensions/mssql/src/objectManagement/ui/objectManagementDialogBase.ts b/extensions/mssql/src/objectManagement/ui/objectManagementDialogBase.ts index 817e607132..7cbf361b29 100644 --- a/extensions/mssql/src/objectManagement/ui/objectManagementDialogBase.ts +++ b/extensions/mssql/src/objectManagement/ui/objectManagementDialogBase.ts @@ -3,36 +3,19 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -// TODO: -// 1. include server properties and other properties in the telemetry. - import * as azdata from 'azdata'; import { IObjectManagementService, ObjectManagement } from 'mssql'; import * as vscode from 'vscode'; -import { EOL } from 'os'; import { generateUuid } from 'vscode-languageclient/lib/utils/uuid'; -import { getErrorMessage } from '../../utils'; -import { TelemetryActions, ObjectManagementViewName } from '../constants'; -import { - CreateObjectOperationDisplayName, HelpText, LoadingDialogText, - NameText, - NewObjectDialogTitle, NoActionScriptedMessage, ObjectPropertiesDialogTitle, OkText, ScriptError, ScriptGeneratedText, ScriptText, SelectedText, UpdateObjectOperationDisplayName -} from '../localizedConstants'; -import { deepClone, getNodeTypeDisplayName, refreshNode } from '../utils'; +import * as localizedConstants from '../localizedConstants'; +import { deepClone, getNodeTypeDisplayName, refreshNode, refreshParentNode } from '../utils'; +import { DialogBase } from './dialogBase'; +import { ObjectManagementViewName, TelemetryActions } from '../constants'; import { TelemetryReporter } from '../../telemetry'; +import { getErrorMessage } from '../../utils'; import { providerId } from '../../constants'; +import { equals } from '../../util/objects'; -export const DefaultLabelWidth = 150; -export const DefaultInputWidth = 300; -export const DefaultTableWidth = DefaultInputWidth + DefaultLabelWidth; -export const DefaultTableMaxHeight = 400; -export const DefaultTableMinRowCount = 2; -export const TableRowHeight = 25; -export const TableColumnHeaderHeight = 30; - -export function getTableHeight(rowCount: number, minRowCount: number = DefaultTableMinRowCount, maxHeight: number = DefaultTableMaxHeight): number { - return Math.min(Math.max(rowCount, minRowCount) * TableRowHeight + TableColumnHeaderHeight, maxHeight); -} function getDialogName(type: ObjectManagement.NodeType, isNewObject: boolean): string { return isNewObject ? `New${type}` : `${type}Properties` @@ -50,56 +33,90 @@ export interface ObjectManagementDialogOptions { objectName?: string; } -export abstract class ObjectManagementDialogBase> { - protected readonly disposables: vscode.Disposable[] = []; - protected readonly dialogObject: azdata.window.Dialog; +export abstract class ObjectManagementDialogBase> extends DialogBase { private _contextId: string; private _viewInfo: ViewInfoType; private _originalObjectInfo: ObjectInfoType; - private _modelView: azdata.ModelView; - private _loadingComponent: azdata.LoadingComponent; - private _formContainer: azdata.DivContainer; private _helpButton: azdata.window.Button; private _scriptButton: azdata.window.Button; constructor(protected readonly objectManagementService: IObjectManagementService, protected readonly options: ObjectManagementDialogOptions) { - this.options.width = this.options.width || 'narrow'; - const objectTypeDisplayName = getNodeTypeDisplayName(options.objectType, true); - const dialogTitle = options.isNewObject ? NewObjectDialogTitle(objectTypeDisplayName) : ObjectPropertiesDialogTitle(objectTypeDisplayName, options.objectName); - this.dialogObject = azdata.window.createModelViewDialog(dialogTitle, getDialogName(options.objectType, options.isNewObject), options.width); - this.dialogObject.okButton.label = OkText; - this.disposables.push(this.dialogObject.onClosed(async (reason: azdata.window.CloseReason) => { await this.dispose(reason); })); - this._helpButton = azdata.window.createButton(HelpText, 'left'); + super(options.isNewObject ? localizedConstants.NewObjectDialogTitle(getNodeTypeDisplayName(options.objectType, true)) : + localizedConstants.ObjectPropertiesDialogTitle(getNodeTypeDisplayName(options.objectType, true), options.objectName), + getDialogName(options.objectType, options.isNewObject), + options.width || 'narrow', 'flyout' + ); + this._helpButton = azdata.window.createButton(localizedConstants.HelpText, 'left'); this.disposables.push(this._helpButton.onClick(async () => { await vscode.commands.executeCommand('vscode.open', vscode.Uri.parse(this.docUrl)); })); - this._scriptButton = azdata.window.createButton(ScriptText, 'left'); + this._scriptButton = azdata.window.createButton(localizedConstants.ScriptText, 'left'); this.disposables.push(this._scriptButton.onClick(async () => { await this.onScriptButtonClick(); })); this.dialogObject.customButtons = [this._helpButton, this._scriptButton]; - this.updateLoadingStatus(true); this._contextId = generateUuid(); - this.dialogObject.registerCloseValidator(async (): Promise => { - const confirmed = await this.onConfirmation(); - if (!confirmed) { - return false; - } - return await this.runValidation(); - }); } protected abstract initializeUI(): Promise; - protected abstract validateInput(): Promise; + protected abstract get docUrl(): string; - protected postInitializeData(): void { + protected postInitializeData(): void { } - } - protected onObjectValueChange(): void { + protected override onFormFieldChange(): void { + this._scriptButton.enabled = this.isDirty; this.dialogObject.okButton.enabled = this.isDirty; } - protected async onConfirmation(): Promise { - return true; + protected override async validateInput(): Promise { + const errors: string[] = []; + if (!this.objectInfo.name) { + errors.push(localizedConstants.NameCannotBeEmptyError); + } + return errors; + } + + protected override async initialize(): Promise { + await this.initializeData(); + await this.initializeUI(); + const typeDisplayName = getNodeTypeDisplayName(this.options.objectType); + this.dialogObject.registerOperation({ + displayName: this.options.isNewObject ? localizedConstants.CreateObjectOperationDisplayName(typeDisplayName) + : localizedConstants.UpdateObjectOperationDisplayName(typeDisplayName, this.options.objectName), + description: '', + isCancelable: false, + operation: async (operation: azdata.BackgroundOperation): Promise => { + const actionName = this.options.isNewObject ? TelemetryActions.CreateObject : TelemetryActions.UpdateObject; + try { + if (this.isDirty) { + const startTime = Date.now(); + await this.objectManagementService.save(this._contextId, this.objectInfo); + if (this.options.objectExplorerContext) { + if (this.options.isNewObject) { + await refreshNode(this.options.objectExplorerContext); + } else { + // For edit mode, the node context is the object itself, we need to refresh the parent node to reflect the changes. + await refreshParentNode(this.options.objectExplorerContext); + } + } + + TelemetryReporter.sendTelemetryEvent(actionName, { + objectType: this.options.objectType + }, { + elapsedTimeMs: Date.now() - startTime + }); + operation.updateStatus(azdata.TaskStatus.Succeeded); + } + } + catch (err) { + operation.updateStatus(azdata.TaskStatus.Failed, getErrorMessage(err)); + TelemetryReporter.createErrorEvent2(ObjectManagementViewName, actionName, err).withAdditionalProperties({ + objectType: this.options.objectType + }).send(); + } finally { + await this.disposeView(); + } + } + }); } protected get viewInfo(): ViewInfoType { @@ -114,85 +131,12 @@ export abstract class ObjectManagementDialogBase { - try { - const initializeViewPromise = new Promise((async resolve => { - await this.dialogObject.registerContent(async view => { - this._modelView = view; - resolve(); - this._formContainer = this.createFormContainer([]); - this._loadingComponent = view.modelBuilder.loadingComponent().withItem(this._formContainer).withProps({ - loading: true, - loadingText: LoadingDialogText, - showText: true, - CSSStyles: { - width: "100%", - height: "100%" - } - }).component(); - await view.initializeModel(this._loadingComponent); - }); - })); - azdata.window.openDialog(this.dialogObject); - await this.initializeData(); - await initializeViewPromise; - await this.initializeUI(); - this._originalObjectInfo = deepClone(this.objectInfo); - const typeDisplayName = getNodeTypeDisplayName(this.options.objectType); - this.dialogObject.registerOperation({ - displayName: this.options.isNewObject ? CreateObjectOperationDisplayName(typeDisplayName) - : UpdateObjectOperationDisplayName(typeDisplayName, this.options.objectName), - description: '', - isCancelable: false, - operation: async (operation: azdata.BackgroundOperation): Promise => { - const actionName = this.options.isNewObject ? TelemetryActions.CreateObject : TelemetryActions.UpdateObject; - try { - if (JSON.stringify(this.objectInfo) !== JSON.stringify(this._originalObjectInfo)) { - const startTime = Date.now(); - await this.objectManagementService.save(this._contextId, this.objectInfo); - if (this.options.isNewObject && this.options.objectExplorerContext) { - await refreshNode(this.options.objectExplorerContext); - } - - TelemetryReporter.sendTelemetryEvent(actionName, { - objectType: this.options.objectType - }, { - elapsedTimeMs: Date.now() - startTime - }); - operation.updateStatus(azdata.TaskStatus.Succeeded); - } - } - catch (err) { - operation.updateStatus(azdata.TaskStatus.Failed, getErrorMessage(err)); - TelemetryReporter.createErrorEvent2(ObjectManagementViewName, actionName, err).withAdditionalProperties({ - objectType: this.options.objectType - }).send(); - } finally { - await this.disposeView(); - } - } - }); - this.updateLoadingStatus(false); - } catch (err) { - const actionName = this.options.isNewObject ? TelemetryActions.OpenNewObjectDialog : TelemetryActions.OpenPropertiesDialog; - TelemetryReporter.createErrorEvent2(ObjectManagementViewName, actionName, err).withAdditionalProperties({ - objectType: this.options.objectType - }).send(); - void vscode.window.showErrorMessage(getErrorMessage(err)); - azdata.window.closeDialog(this.dialogObject); - } - } - - private async dispose(reason: azdata.window.CloseReason): Promise { - this.disposables.forEach(disposable => disposable.dispose()); + protected override async dispose(reason: azdata.window.CloseReason): Promise { + await super.dispose(reason); if (reason !== 'ok') { await this.disposeView(); } @@ -206,140 +150,17 @@ export abstract class ObjectManagementDialogBase { - const errors = await this.validateInput(); - if (errors.length > 0 && (this.dialogObject.message?.text || showErrorMessage)) { - this.dialogObject.message = { - text: errors.join(EOL), - level: azdata.window.MessageLevel.Error - }; - } else { - this.dialogObject.message = undefined; - } - return errors.length === 0; - } - - protected createLabelInputContainer(label: string, input: azdata.InputBoxComponent | azdata.DropDownComponent): azdata.FlexContainer { - const labelComponent = this.modelView.modelBuilder.text().withProps({ width: DefaultLabelWidth, value: label, requiredIndicator: input.required }).component(); - const row = this.modelView.modelBuilder.flexContainer().withLayout({ flexFlow: 'horizontal', flexWrap: 'nowrap', alignItems: 'center' }).withItems([labelComponent, input]).component(); - return row; - } - - protected createCheckbox(label: string, checked: boolean = false, enabled: boolean = true): azdata.CheckBoxComponent { - return this.modelView.modelBuilder.checkBox().withProps({ - label: label, - checked: checked, - enabled: enabled - }).component(); - } - - protected createPasswordInputBox(ariaLabel: string, value: string = '', enabled: boolean = true, width: number = DefaultInputWidth): azdata.InputBoxComponent { - return this.createInputBox(ariaLabel, value, enabled, 'password', width); - } - - protected createInputBox(ariaLabel: string, value: string = '', enabled: boolean = true, type: azdata.InputBoxInputType = 'text', width: number = DefaultInputWidth): azdata.InputBoxComponent { - return this.modelView.modelBuilder.inputBox().withProps({ inputType: type, enabled: enabled, ariaLabel: ariaLabel, value: value, width: width }).component(); - } - - protected createGroup(header: string, items: azdata.Component[], collapsible: boolean = true, collapsed: boolean = false): azdata.GroupContainer { - return this.modelView.modelBuilder.groupContainer().withLayout({ - header: header, - collapsible: collapsible, - collapsed: collapsed - }).withItems(items).component(); - } - - protected createFormContainer(items: azdata.Component[]): azdata.DivContainer { - return this.modelView.modelBuilder.divContainer().withLayout({ width: 'calc(100% - 20px)', height: 'calc(100% - 20px)' }).withProps({ - CSSStyles: { 'padding': '10px' } - }).withItems(items, { CSSStyles: { 'margin-block-end': '10px' } }).component(); - } - - protected createTableList(ariaLabel: string, listValues: string[], selectedValues: string[], data?: any[][]): azdata.TableComponent { - let tableData = data; - if (tableData === undefined) { - tableData = listValues.map(name => { - const isSelected = selectedValues.indexOf(name) !== -1; - return [isSelected, name]; - }); - } - const table = this.modelView.modelBuilder.table().withProps( - { - ariaLabel: ariaLabel, - data: tableData, - columns: [ - { - value: SelectedText, - type: azdata.ColumnType.checkBox, - options: { actionOnCheckbox: azdata.ActionOnCellCheckboxCheck.customAction } - }, { - value: NameText, - } - ], - width: DefaultTableWidth, - height: getTableHeight(tableData.length) - } - ).component(); - this.disposables.push(table.onCellAction!((arg: azdata.ICheckboxCellActionEventArgs) => { - const name = listValues[arg.row]; - const idx = selectedValues.indexOf(name); - if (arg.checked && idx === -1) { - selectedValues.push(name); - } else if (!arg.checked && idx !== -1) { - selectedValues.splice(idx, 1) - } - this.onObjectValueChange(); - })); - return table; - } - - protected createDropdown(ariaLabel: string, values: string[], value: string | undefined, enabled: boolean = true, width: number = DefaultInputWidth): azdata.DropDownComponent { - // Automatically add an empty item to the beginning of the list if the current value is not specified. - // This is needed when no meaningful default value can be provided. - // Create a new array so that the original array isn't modified. - const dropdownValues = []; - dropdownValues.push(...values); - if (!value) { - dropdownValues.unshift(''); - } - return this.modelView.modelBuilder.dropDown().withProps({ - ariaLabel: ariaLabel, - values: dropdownValues, - value: value, - width: width, - enabled: enabled - }).component(); - } - - protected removeItem(container: azdata.DivContainer | azdata.FlexContainer, item: azdata.Component): void { - if (container.items.indexOf(item) !== -1) { - container.removeItem(item); - } - } - - protected addItem(container: azdata.DivContainer | azdata.FlexContainer, item: azdata.Component, index?: number): void { - if (container.items.indexOf(item) === -1) { - if (index === undefined) { - container.addItem(item); - } else { - container.insertItem(item, index); - } - } - } - - private updateLoadingStatus(isLoading: boolean): void { - this._scriptButton.enabled = !isLoading; + protected override onLoadingStatusChanged(isLoading: boolean): void { + super.onLoadingStatusChanged(isLoading); this._helpButton.enabled = !isLoading; - this.dialogObject.okButton.enabled = isLoading ? false : this.isDirty; - if (this._loadingComponent) { - this._loadingComponent.loading = isLoading; - } + this.dialogObject.okButton.enabled = this._scriptButton.enabled = isLoading ? false : this.isDirty; } private async onScriptButtonClick(): Promise { - this.updateLoadingStatus(true); + this.onLoadingStatusChanged(true); try { const isValid = await this.runValidation(); if (!isValid) { @@ -348,10 +169,10 @@ export abstract class ObjectManagementDialogBase { + // Sections + private generalSection: azdata.GroupContainer; + private membershipSection: azdata.GroupContainer; + private memberSection: azdata.GroupContainer; + + // General section content + private nameInput: azdata.InputBoxComponent; + private ownerInput: azdata.InputBoxComponent; + + // Member section content + private memberTable: azdata.TableComponent; + + // Membership section content + private membershipTable: azdata.TableComponent; + + + constructor(objectManagementService: IObjectManagementService, options: ObjectManagementDialogOptions) { + super(objectManagementService, options); + } + + protected override get docUrl(): string { + return this.options.isNewObject ? CreateServerRoleDocUrl : AlterServerRoleDocUrl; + } + + protected async initializeUI(): Promise { + this.initializeGeneralSection(); + this.initializeMemberSection(); + const sections: azdata.Component[] = [this.generalSection, this.memberSection]; + if (!this.viewInfo.isFixedRole) { + this.initializeMembershipSection(); + sections.push(this.membershipSection); + } + this.formContainer.addItems(sections); + } + + private initializeGeneralSection(): void { + this.nameInput = this.createInputBox(localizedConstants.NameText, async (newValue) => { + this.objectInfo.name = newValue; + }, this.objectInfo.name, this.options.isNewObject); + const nameContainer = this.createLabelInputContainer(localizedConstants.NameText, this.nameInput); + + this.ownerInput = this.createInputBox(localizedConstants.OwnerText, async (newValue) => { + this.objectInfo.owner = newValue; + }, this.objectInfo.owner, !this.viewInfo.isFixedRole, 'text', 210); + const browseOwnerButton = this.createButton(localizedConstants.BrowseText, localizedConstants.BrowseOwnerButtonAriaLabel, async () => { + const dialog = new FindObjectDialog(this.objectManagementService, { + objectTypes: [ObjectManagement.NodeType.ServerLevelLogin, ObjectManagement.NodeType.ServerLevelServerRole], + multiSelect: false, + contextId: this.contextId, + title: localizedConstants.SelectServerRoleOwnerDialogTitle + }); + await dialog.open(); + const result = await dialog.waitForClose(); + if (result.selectedObjects.length > 0) { + this.ownerInput.value = result.selectedObjects[0].name; + } + }, !this.viewInfo.isFixedRole); + const ownerContainer = this.createLabelInputContainer(localizedConstants.OwnerText, this.ownerInput); + ownerContainer.addItems([browseOwnerButton], { flex: '0 0 auto' }); + this.generalSection = this.createGroup(localizedConstants.GeneralSectionHeader, [ + nameContainer, + ownerContainer + ], false); + } + + private initializeMemberSection(): void { + this.memberTable = this.createTable(localizedConstants.MemberSectionHeader, [ + { + type: azdata.ColumnType.text, + value: 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: [ObjectManagement.NodeType.ServerLevelLogin, ObjectManagement.NodeType.ServerLevelServerRole], + multiSelect: true, + contextId: this.contextId, + title: localizedConstants.SelectServerRoleMemberDialogTitle + }); + await dialog.open(); + const result = await dialog.waitForClose(); + this.addMembers(result.selectedObjects.map(r => r.name)); + }, + async () => { + if (this.memberTable.selectedRows.length === 1) { + this.removeMember(this.memberTable.selectedRows[0]); + } + }); + this.memberSection = this.createGroup(localizedConstants.MemberSectionHeader, [this.memberTable, buttonContainer]); + } + + private addMembers(names: string[]): void { + names.forEach(n => { + if (this.objectInfo.members.indexOf(n) === -1) { + this.objectInfo.members.push(n); + } + }); + this.updateMembersTable(); + } + + private removeMember(idx: number): void { + this.objectInfo.members.splice(idx, 1); + this.updateMembersTable(); + } + + private updateMembersTable(): void { + this.setTableData(this.memberTable, this.objectInfo.members.map(m => [m])); + this.onFormFieldChange(); + } + + private initializeMembershipSection(): void { + this.membershipTable = this.createTableList(localizedConstants.MembershipSectionHeader, [localizedConstants.ServerRoleTypeDisplayNameInTitle], this.viewInfo.serverRoles, this.objectInfo.memberships); + this.membershipSection = this.createGroup(localizedConstants.MembershipSectionHeader, [this.membershipTable]); + } +} diff --git a/extensions/mssql/src/objectManagement/ui/userDialog.ts b/extensions/mssql/src/objectManagement/ui/userDialog.ts index 5db090ca64..d2d625b73f 100644 --- a/extensions/mssql/src/objectManagement/ui/userDialog.ts +++ b/extensions/mssql/src/objectManagement/ui/userDialog.ts @@ -3,11 +3,12 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as azdata from 'azdata'; -import { DefaultInputWidth, ObjectManagementDialogBase, ObjectManagementDialogOptions } from './objectManagementDialogBase'; +import { ObjectManagementDialogBase, ObjectManagementDialogOptions } from './objectManagementDialogBase'; import { IObjectManagementService, ObjectManagement } from 'mssql'; import * as localizedConstants from '../localizedConstants'; import { AlterUserDocUrl, CreateUserDocUrl } from '../constants'; import { getAuthenticationTypeByDisplayName, getAuthenticationTypeDisplayName, getUserTypeByDisplayName, getUserTypeDisplayName, isValidSQLPassword } from '../utils'; +import { DefaultMaxTableHeight } from './dialogBase'; export class UserDialog extends ObjectManagementDialogBase { private generalSection: azdata.GroupContainer; @@ -43,11 +44,8 @@ export class UserDialog extends ObjectManagementDialogBase { - const errors: string[] = []; - if (!this.objectInfo.name) { - errors.push(localizedConstants.NameCannotBeEmptyError); - } + protected override async validateInput(): Promise { + const errors = await super.validateInput(); if (this.objectInfo.type === ObjectManagement.UserType.Contained && this.objectInfo.authenticationType === ObjectManagement.AuthenticationType.Sql) { if (!this.objectInfo.password) { errors.push(localizedConstants.PasswordCannotBeEmptyError); @@ -79,41 +77,27 @@ export class UserDialog extends ObjectManagementDialogBase { - this.objectInfo.name = this.nameInput.value!; - this.onObjectValueChange(); - await this.runValidation(false); - })); + this.nameInput = this.createInputBox(localizedConstants.NameText, async (newValue) => { + this.objectInfo.name = newValue; + }, this.objectInfo.name, this.options.isNewObject); const nameContainer = this.createLabelInputContainer(localizedConstants.NameText, this.nameInput); - this.defaultSchemaDropdown = this.createDropdown(localizedConstants.DefaultSchemaText, this.viewInfo.schemas, this.objectInfo.defaultSchema!); + + this.defaultSchemaDropdown = this.createDropdown(localizedConstants.DefaultSchemaText, async (newValue) => { + this.objectInfo.defaultSchema = newValue; + }, this.viewInfo.schemas, this.objectInfo.defaultSchema!); this.defaultSchemaContainer = this.createLabelInputContainer(localizedConstants.DefaultSchemaText, this.defaultSchemaDropdown); - this.disposables.push(this.defaultSchemaDropdown.onValueChanged(() => { - this.objectInfo.defaultSchema = this.defaultSchemaDropdown.value; - this.onObjectValueChange(); - })); // only supporting user with login for initial preview const userTypes = [localizedConstants.UserWithLoginText, localizedConstants.UserWithWindowsGroupLoginText, localizedConstants.ContainedUserText, localizedConstants.UserWithNoConnectAccess]; - this.typeDropdown = this.createDropdown(localizedConstants.UserTypeText, userTypes, getUserTypeDisplayName(this.objectInfo.type), this.options.isNewObject); - this.disposables.push(this.typeDropdown.onValueChanged(async () => { - this.objectInfo.type = getUserTypeByDisplayName(this.typeDropdown.value); - this.onObjectValueChange(); + this.typeDropdown = this.createDropdown(localizedConstants.UserTypeText, async (newValue) => { + this.objectInfo.type = getUserTypeByDisplayName(newValue); this.setViewByUserType(); - await this.runValidation(false); - })); + }, userTypes, getUserTypeDisplayName(this.objectInfo.type), this.options.isNewObject); this.typeContainer = this.createLabelInputContainer(localizedConstants.UserTypeText, this.typeDropdown); - this.loginDropdown = this.createDropdown(localizedConstants.LoginText, this.viewInfo.logins, this.objectInfo.loginName, this.options.isNewObject); - this.disposables.push(this.loginDropdown.onValueChanged(async () => { - this.objectInfo.loginName = this.loginDropdown.value; - this.onObjectValueChange(); - await this.runValidation(false); - })); + + this.loginDropdown = this.createDropdown(localizedConstants.LoginText, async (newValue) => { + this.objectInfo.loginName = newValue; + }, this.viewInfo.logins, this.objectInfo.loginName, this.options.isNewObject); this.loginContainer = this.createLabelInputContainer(localizedConstants.LoginText, this.loginDropdown); const authTypes = []; @@ -126,27 +110,18 @@ export class UserDialog extends ObjectManagementDialogBase { - this.objectInfo.authenticationType = getAuthenticationTypeByDisplayName(this.authTypeDropdown.value); - this.onObjectValueChange(); + this.authTypeDropdown = this.createDropdown(localizedConstants.AuthTypeText, async (newValue) => { + this.objectInfo.authenticationType = getAuthenticationTypeByDisplayName(newValue); this.setViewByAuthenticationType(); - await this.runValidation(false); - })); + }, authTypes, getAuthenticationTypeDisplayName(this.objectInfo.authenticationType), this.options.isNewObject); + this.authTypeContainer = this.createLabelInputContainer(localizedConstants.AuthTypeText, this.authTypeDropdown); - this.passwordInput = this.createPasswordInputBox(localizedConstants.PasswordText, this.objectInfo.password ?? ''); + this.passwordInput = this.createPasswordInputBox(localizedConstants.PasswordText, async (newValue) => { + this.objectInfo.password = newValue; + }, this.objectInfo.password ?? ''); this.passwordContainer = this.createLabelInputContainer(localizedConstants.PasswordText, this.passwordInput); - this.confirmPasswordInput = this.createPasswordInputBox(localizedConstants.ConfirmPasswordText, this.objectInfo.password ?? ''); + this.confirmPasswordInput = this.createPasswordInputBox(localizedConstants.ConfirmPasswordText, async () => { }, this.objectInfo.password ?? ''); this.confirmPasswordContainer = this.createLabelInputContainer(localizedConstants.ConfirmPasswordText, this.confirmPasswordInput); - this.disposables.push(this.passwordInput.onTextChanged(async () => { - this.objectInfo.password = this.passwordInput.value; - this.onObjectValueChange(); - await this.runValidation(false); - })); - this.disposables.push(this.confirmPasswordInput.onTextChanged(async () => { - await this.runValidation(false); - })); this.generalSection = this.createGroup(localizedConstants.GeneralSectionHeader, [ nameContainer, @@ -160,25 +135,27 @@ export class UserDialog extends ObjectManagementDialogBase { - const isSelected = this.objectInfo.ownedSchemas.indexOf(name) !== -1; - return [{ enabled: !isSelected, checked: isSelected }, name]; - }); - this.ownedSchemaTable = this.createTableList(localizedConstants.OwnedSchemaSectionHeader, this.viewInfo.schemas, this.objectInfo.ownedSchemas, ownedSchemaData); + this.ownedSchemaTable = this.createTableList(localizedConstants.OwnedSchemaSectionHeader, + [localizedConstants.SchemaText], + this.viewInfo.schemas, + this.objectInfo.ownedSchemas, + DefaultMaxTableHeight, + (item) => { + // It is not allowed to have unassigned schema. + return this.objectInfo.ownedSchemas.indexOf(item) === -1; + }); this.ownedSchemaSection = this.createGroup(localizedConstants.OwnedSchemaSectionHeader, [this.ownedSchemaTable]); } private initializeMembershipSection(): void { - this.membershipTable = this.createTableList(localizedConstants.MembershipSectionHeader, this.viewInfo.databaseRoles, this.objectInfo.databaseRoles); + this.membershipTable = this.createTableList(localizedConstants.MembershipSectionHeader, [localizedConstants.DatabaseRoleTypeDisplayNameInTitle], this.viewInfo.databaseRoles, this.objectInfo.databaseRoles); this.membershipSection = this.createGroup(localizedConstants.MembershipSectionHeader, [this.membershipTable]); } private initializeAdvancedSection(): void { - this.defaultLanguageDropdown = this.createDropdown(localizedConstants.DefaultLanguageText, this.viewInfo.languages, this.objectInfo.defaultLanguage); - this.disposables.push(this.defaultLanguageDropdown.onValueChanged(() => { - this.objectInfo.defaultLanguage = this.defaultLanguageDropdown.value; - this.onObjectValueChange(); - })); + this.defaultLanguageDropdown = this.createDropdown(localizedConstants.DefaultLanguageText, async (newValue) => { + this.objectInfo.defaultLanguage = newValue; + }, this.viewInfo.languages, this.objectInfo.defaultLanguage); const container = this.createLabelInputContainer(localizedConstants.DefaultLanguageText, this.defaultLanguageDropdown); this.advancedSection = this.createGroup(localizedConstants.AdvancedSectionHeader, [container]); } diff --git a/extensions/mssql/src/objectManagement/utils.ts b/extensions/mssql/src/objectManagement/utils.ts index 56fc9b4c08..de6f104dfe 100644 --- a/extensions/mssql/src/objectManagement/utils.ts +++ b/extensions/mssql/src/objectManagement/utils.ts @@ -7,7 +7,7 @@ import * as azdata from 'azdata'; import * as vscode from 'vscode'; import { getErrorMessage } from '../utils'; import { ObjectManagement } from 'mssql'; -import { AADAuthenticationTypeDisplayText, ColumnTypeDisplayName, ContainedUserText, DatabaseTypeDisplayName, LoginTypeDisplayName, LoginTypeDisplayNameInTitle, RefreshObjectExplorerError, SQLAuthenticationTypeDisplayText, TableTypeDisplayName, UserTypeDisplayName, UserTypeDisplayNameInTitle, UserWithLoginText, UserWithNoConnectAccess, UserWithWindowsGroupLoginText, ViewTypeDisplayName, WindowsAuthenticationTypeDisplayText } from './localizedConstants'; +import * as localizedConstants from './localizedConstants'; export function deepClone(obj: T): T { if (!obj || typeof obj !== 'object') { @@ -36,7 +36,7 @@ export async function refreshParentNode(context: azdata.ObjectExplorerContext): await parentNode?.refresh(); } catch (err) { - await vscode.window.showErrorMessage(RefreshObjectExplorerError(getErrorMessage(err))); + await vscode.window.showErrorMessage(localizedConstants.RefreshObjectExplorerError(getErrorMessage(err))); } } } @@ -48,27 +48,33 @@ export async function refreshNode(context: azdata.ObjectExplorerContext): Promis await node?.refresh(); } catch (err) { - await vscode.window.showErrorMessage(RefreshObjectExplorerError(getErrorMessage(err))); + await vscode.window.showErrorMessage(localizedConstants.RefreshObjectExplorerError(getErrorMessage(err))); } } } export function getNodeTypeDisplayName(type: string, inTitle: boolean = false): string { switch (type) { + case ObjectManagement.NodeType.ApplicationRole: + return inTitle ? localizedConstants.ApplicationRoleTypeDisplayNameInTitle : localizedConstants.ApplicationRoleTypeDisplayName; + case ObjectManagement.NodeType.DatabaseRole: + return inTitle ? localizedConstants.DatabaseRoleTypeDisplayNameInTitle : localizedConstants.DatabaseRoleTypeDisplayName; case ObjectManagement.NodeType.ServerLevelLogin: - return inTitle ? LoginTypeDisplayNameInTitle : LoginTypeDisplayName; + return inTitle ? localizedConstants.LoginTypeDisplayNameInTitle : localizedConstants.LoginTypeDisplayName; + case ObjectManagement.NodeType.ServerLevelServerRole: + return inTitle ? localizedConstants.ServerRoleTypeDisplayNameInTitle : localizedConstants.ServerRoleTypeDisplayName; case ObjectManagement.NodeType.User: - return inTitle ? UserTypeDisplayNameInTitle : UserTypeDisplayName; + return inTitle ? localizedConstants.UserTypeDisplayNameInTitle : localizedConstants.UserTypeDisplayName; case ObjectManagement.NodeType.Table: - return TableTypeDisplayName; + return localizedConstants.TableTypeDisplayName; case ObjectManagement.NodeType.View: - return ViewTypeDisplayName; + return localizedConstants.ViewTypeDisplayName; case ObjectManagement.NodeType.Column: - return ColumnTypeDisplayName; + return localizedConstants.ColumnTypeDisplayName; case ObjectManagement.NodeType.Database: - return DatabaseTypeDisplayName; + return localizedConstants.DatabaseTypeDisplayName; default: - throw new Error(`Unkown node type: ${type}`); + throw new Error(`Unknown node type: ${type}`); } } @@ -77,19 +83,19 @@ export function getAuthenticationTypeDisplayName(authType: ObjectManagement.Auth switch (authType) { case ObjectManagement.AuthenticationType.Windows: - return WindowsAuthenticationTypeDisplayText; + return localizedConstants.WindowsAuthenticationTypeDisplayText; case ObjectManagement.AuthenticationType.AzureActiveDirectory: - return AADAuthenticationTypeDisplayText; + return localizedConstants.AADAuthenticationTypeDisplayText; default: - return SQLAuthenticationTypeDisplayText; + return localizedConstants.SQLAuthenticationTypeDisplayText; } } export function getAuthenticationTypeByDisplayName(displayValue: string): ObjectManagement.AuthenticationType { switch (displayValue) { - case WindowsAuthenticationTypeDisplayText: + case localizedConstants.WindowsAuthenticationTypeDisplayText: return ObjectManagement.AuthenticationType.Windows; - case AADAuthenticationTypeDisplayText: + case localizedConstants.AADAuthenticationTypeDisplayText: return ObjectManagement.AuthenticationType.AzureActiveDirectory; default: return ObjectManagement.AuthenticationType.Sql; @@ -99,23 +105,23 @@ export function getAuthenticationTypeByDisplayName(displayValue: string): Object export function getUserTypeDisplayName(userType: ObjectManagement.UserType): string { switch (userType) { case ObjectManagement.UserType.WithLogin: - return UserWithLoginText; + return localizedConstants.UserWithLoginText; case ObjectManagement.UserType.WithWindowsGroupLogin: - return UserWithWindowsGroupLoginText; + return localizedConstants.UserWithWindowsGroupLoginText; case ObjectManagement.UserType.Contained: - return ContainedUserText; + return localizedConstants.ContainedUserText; default: - return UserWithNoConnectAccess; + return localizedConstants.UserWithNoConnectAccess; } } export function getUserTypeByDisplayName(userTypeDisplayName: string): ObjectManagement.UserType { switch (userTypeDisplayName) { - case UserWithLoginText: + case localizedConstants.UserWithLoginText: return ObjectManagement.UserType.WithLogin; - case UserWithWindowsGroupLoginText: + case localizedConstants.UserWithWindowsGroupLoginText: return ObjectManagement.UserType.WithWindowsGroupLogin; - case ContainedUserText: + case localizedConstants.ContainedUserText: return ObjectManagement.UserType.Contained; default: return ObjectManagement.UserType.NoConnectAccess; @@ -124,7 +130,7 @@ export function getUserTypeByDisplayName(userTypeDisplayName: string): ObjectMan // https://docs.microsoft.com/sql/relational-databases/security/password-policy export function isValidSQLPassword(password: string, userName: string = 'sa'): boolean { - const containsUserName = password && userName !== undefined && password.toUpperCase().includes(userName.toUpperCase()); + const containsUserName = password && userName && password.toUpperCase().includes(userName.toUpperCase()); const hasUpperCase = /[A-Z]/.test(password) ? 1 : 0; const hasLowerCase = /[a-z]/.test(password) ? 1 : 0; const hasNumbers = /\d/.test(password) ? 1 : 0; diff --git a/extensions/mssql/src/util/objects.ts b/extensions/mssql/src/util/objects.ts new file mode 100644 index 0000000000..e2dc1febbc --- /dev/null +++ b/extensions/mssql/src/util/objects.ts @@ -0,0 +1,70 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +export function equals(one: any, other: any, strictArrayCompare: boolean = true): boolean { + if (one === other) { + return true; + } + if (one === null || one === undefined || other === null || other === undefined) { + return false; + } + if (typeof one !== typeof other) { + return false; + } + if (typeof one !== 'object') { + return false; + } + if ((Array.isArray(one)) !== (Array.isArray(other))) { + return false; + } + + let i: number; + let key: string; + + if (Array.isArray(one)) { + if (one.length !== other.length) { + return false; + } + for (i = 0; i < one.length; i++) { + if (strictArrayCompare) { + if (!equals(one[i], other[i], strictArrayCompare)) { + return false; + } + } else { + let match = false; + for (let j = 0; j < other.length; j++) { + if (equals(one[i], other[j], strictArrayCompare)) { + match = true; + break; + } + } + if (!match) { + return false; + } + } + } + } else { + const oneKeys: string[] = []; + + for (key in one) { + oneKeys.push(key); + } + oneKeys.sort(); + const otherKeys: string[] = []; + for (key in other) { + otherKeys.push(key); + } + otherKeys.sort(); + if (!equals(oneKeys, otherKeys, strictArrayCompare)) { + return false; + } + for (i = 0; i < oneKeys.length; i++) { + if (!equals(one[oneKeys[i]], other[oneKeys[i]], strictArrayCompare)) { + return false; + } + } + } + return true; +}