From b56f2ccb60c620857dd1d51f8c8a1eb42efef4f1 Mon Sep 17 00:00:00 2001 From: Alan Ren Date: Mon, 15 May 2023 15:01:57 -0700 Subject: [PATCH] add securable settings (#22936) * wip * Update typings * nullable * update test service * support securables * updata test data * fix issues * fix build failure * update test mocks * fix typo * fix reference * fix findobjectdialog issue * update SearchResultItem type * fix table component perf issue * hide effective permission for server role * hide effective permission for app role and db role * vbump sts and fix a couple issues * STS update and UI update * fix user login display issue * vbump sts --- extensions/dacpac/src/test/testContext.ts | 3 +- .../datavirtualization/src/test/stubs.ts | 3 + extensions/mssql/config.json | 2 +- extensions/mssql/src/mssql.d.ts | 111 +++++++-- .../objectManagement/localizedConstants.ts | 18 +- .../objectManagementService.ts | 182 ++++++++++++-- .../ui/applicationRoleDialog.ts | 16 +- .../objectManagement/ui/databaseRoleDialog.ts | 47 ++-- .../objectManagement/ui/findObjectDialog.ts | 77 +++--- .../src/objectManagement/ui/loginDialog.ts | 21 +- .../ui/principalDialogBase.ts | 228 ++++++++++++++++++ .../objectManagement/ui/serverRoleDialog.ts | 38 +-- .../src/objectManagement/ui/userDialog.ts | 23 +- extensions/mssql/src/ui/dialogBase.ts | 46 ++-- extensions/mssql/src/ui/localizedConstants.ts | 2 +- .../schema-compare/src/test/testContext.ts | 3 +- src/sql/azdata.proposed.d.ts | 7 + .../platform/dashboard/browser/interfaces.ts | 3 +- .../workbench/api/common/extHostModelView.ts | 4 + .../workbench/api/common/sqlExtHostTypes.ts | 3 +- .../modelComponents/table.component.ts | 34 +-- 21 files changed, 693 insertions(+), 178 deletions(-) create mode 100644 extensions/mssql/src/objectManagement/ui/principalDialogBase.ts diff --git a/extensions/dacpac/src/test/testContext.ts b/extensions/dacpac/src/test/testContext.ts index e0c95c1316..ce8e0b1da3 100644 --- a/extensions/dacpac/src/test/testContext.ts +++ b/extensions/dacpac/src/test/testContext.ts @@ -199,7 +199,8 @@ export function createViewContext(): ViewTestContext { data: [] as any[][], columns: [] as string[], onRowSelected: onClick.event, - appendData: (data: any[][]) => undefined, + appendData: (_data: any[][]) => undefined, + setActiveCell: (_row: number, _column: number) => undefined }); let loadingComponent: () => azdata.LoadingComponent = () => Object.assign({}, componentBase, { diff --git a/extensions/datavirtualization/src/test/stubs.ts b/extensions/datavirtualization/src/test/stubs.ts index 28112543b0..14d9cd1747 100644 --- a/extensions/datavirtualization/src/test/stubs.ts +++ b/extensions/datavirtualization/src/test/stubs.ts @@ -301,6 +301,9 @@ export class MockTableComponent extends MockUIComponent implements azdata.TableC appendData(data: any[][]): Thenable { throw new Error('Method not implemented.'); } + setActiveCell(row: number, column: number): void { + throw new Error('Method not implemented.'); + } } export class MockDeclarativeTableComponent extends MockUIComponent implements azdata.DeclarativeTableComponent { diff --git a/extensions/mssql/config.json b/extensions/mssql/config.json index 244d5e2346..238134d240 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.8.0.5", + "version": "4.8.0.7", "downloadFileNames": { "Windows_86": "win-x86-net7.0.zip", "Windows_64": "win-x64-net7.0.zip", diff --git a/extensions/mssql/src/mssql.d.ts b/extensions/mssql/src/mssql.d.ts index 7b5a9b83a5..1bb257213d 100644 --- a/extensions/mssql/src/mssql.d.ts +++ b/extensions/mssql/src/mssql.d.ts @@ -917,7 +917,14 @@ declare module 'mssql' { } /** - * Base interface for the object view information + * Base interface for all the security principal objects. e.g. Login, Server Role, Database Role... + */ + export interface SecurityPrincipalObject extends SqlObject { + securablePermissions: SecurablePermissions[]; + } + + /** + * Base interface for the object view information. */ export interface ObjectViewInfo { /** @@ -926,10 +933,52 @@ declare module 'mssql' { objectInfo: T; } + /** + * Securable type metadata. + */ + export interface SecurableTypeMetadata { + /** + * Name of the securable type. + */ + name: string; + /** + * Display name of the securable type. + */ + displayName: string; + /** + * Permissions supported by the securable type. + */ + permissions: PermissionMetadata[]; + } + + /** + * Permission metadata. + */ + export interface PermissionMetadata { + /** + * Name of the permission. + */ + name: string; + /** + * Display name of the permission. + */ + displayName: string; + } + + /** + * Base interface for security principal object's view information. + */ + export interface SecurityPrincipalViewInfo extends ObjectViewInfo { + /** + * The securable types that the security principal object can be granted permissions on. + */ + supportedSecurableTypes: SecurableTypeMetadata[]; + } + /** * Server level login. */ - export interface Login extends SqlObject { + export interface Login extends SecurityPrincipalObject { /** * Authentication type. */ @@ -1025,7 +1074,7 @@ declare module 'mssql' { /** * The information required to render the login view. */ - export interface LoginViewInfo extends ObjectViewInfo { + export interface LoginViewInfo extends SecurityPrincipalViewInfo { /** * The authentication types supported by the server. */ @@ -1062,20 +1111,24 @@ declare module 'mssql' { /** * The permission information a principal has on a securable. */ - export interface Permission { + export interface SecurablePermissionItem { /** - * Name of the permission. + * name of the permission. */ - name: string; + permission: string; /** - * Whether the permission is granted or denied. + * Name of the grantor. */ - grant: boolean; + grantor: string; + /** + * Whether the permission is granted or denied. Undefined means not specified. + */ + grant?: boolean; /** * Whether the pincipal can grant this permission to other principals. * The value will be ignored if the grant property is set to false. */ - withGrant: boolean; + withGrant?: boolean; } /** @@ -1083,13 +1136,25 @@ declare module 'mssql' { */ export interface SecurablePermissions { /** - * The securable. + * The securable name. */ - securable: SqlObject; + name: string; /** - * The Permissions. + * The securable type. */ - permissions: Permission[]; + type: string; + /** + * The schema name of the object if applicable. + */ + schema?: string; + /** + * The permissions. + */ + permissions: SecurablePermissionItem[]; + /** + * The effective permissions. Includes all permissions granted to the principal, including those granted through role memberships. + */ + effectivePermissions: string[]; } /** @@ -1135,7 +1200,7 @@ declare module 'mssql' { /** * Database user. */ - export interface User extends SqlObject { + export interface User extends SecurityPrincipalObject { /** * Type of the user. */ @@ -1172,7 +1237,7 @@ declare module 'mssql' { /** * The information required to render the user view. */ - export interface UserViewInfo extends ObjectViewInfo { + export interface UserViewInfo extends SecurityPrincipalViewInfo { /** * All user types supported by the database. */ @@ -1198,7 +1263,7 @@ declare module 'mssql' { /** * Interface representing the server role object. */ - export interface ServerRoleInfo extends SqlObject { + export interface ServerRoleInfo extends SecurityPrincipalObject { /** * Name of the server principal that owns the server role. */ @@ -1216,7 +1281,7 @@ declare module 'mssql' { /** * Interface representing the information required to render the server role view. */ - export interface ServerRoleViewInfo extends ObjectViewInfo { + export interface ServerRoleViewInfo extends SecurityPrincipalViewInfo { /** * Whether the server role is a fixed role. */ @@ -1230,7 +1295,7 @@ declare module 'mssql' { /** * Interface representing the application role object. */ - export interface ApplicationRoleInfo extends SqlObject { + export interface ApplicationRoleInfo extends SecurityPrincipalObject { /** * Default schema of the application role. */ @@ -1248,7 +1313,7 @@ declare module 'mssql' { /** * Interface representing the information required to render the application role view. */ - export interface ApplicationRoleViewInfo extends ObjectViewInfo { + export interface ApplicationRoleViewInfo extends SecurityPrincipalViewInfo { /** * List of all the schemas in the database. */ @@ -1258,7 +1323,7 @@ declare module 'mssql' { /** * Interface representing the database role object. */ - export interface DatabaseRoleInfo extends SqlObject { + export interface DatabaseRoleInfo extends SecurityPrincipalObject { /** * Name of the database principal that owns the database role. */ @@ -1276,7 +1341,7 @@ declare module 'mssql' { /** * Interface representing the information required to render the database role view. */ - export interface DatabaseRoleViewInfo extends ObjectViewInfo { + export interface DatabaseRoleViewInfo extends SecurityPrincipalViewInfo { /** * List of all the schemas in the database. */ @@ -1294,7 +1359,7 @@ declare module 'mssql' { /** * type of the object. */ - type: NodeType; + type: string; /** * schema of the object. */ @@ -1369,7 +1434,7 @@ declare module 'mssql' { * @param searchText Search text. * @param schema Schema to search in. */ - search(contextId: string, objectTypes: ObjectManagement.NodeType[], searchText?: string, schema?: string): Thenable; + search(contextId: string, objectTypes: string[], searchText?: string, schema?: string): Thenable; } // Object Management - End. } diff --git a/extensions/mssql/src/objectManagement/localizedConstants.ts b/extensions/mssql/src/objectManagement/localizedConstants.ts index 5ffce7aee9..4b6354ef12 100644 --- a/extensions/mssql/src/objectManagement/localizedConstants.ts +++ b/extensions/mssql/src/objectManagement/localizedConstants.ts @@ -30,9 +30,22 @@ export const RenameObjectDialogTitle: string = localize('objectManagement.rename 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 AddMemberAriaLabel = localize('objectManagement.addMemberText', "Add a member"); +export const AddMemberAriaLabel = localize('objectManagement.addMembersText', "Add members"); export const RemoveMemberAriaLabel = localize('objectManagement.removeMemberText', "Remove selected member"); +export const AddSecurableAriaLabel = localize('objectManagement.addSecurablesText', "Add securables"); +export const RemoveSecurableAriaLabel = localize('objectManagement.removeSecurablesText', "Remove selected securable"); +export const SecurablesText = localize('objectManagement.securablesText', "Securables"); +export const ExplicitPermissionsTableLabel = localize('objectManagement.explicitPermissionsTableLabel', "Explicit permissions for selected securable"); +export const EffectivePermissionsTableLabel = localize('objectManagement.effectivePermissionsTableLabel', "Effective permissions for selected securable"); +export const PermissionColumnHeader = localize('objectManagement.permissionColumnHeader', "Permission"); +export const GrantorColumnHeader = localize('objectManagement.grantorColumnHeader', "Grantor"); +export const GrantColumnHeader = localize('objectManagement.grantColumnHeader', "Grant"); +export const WithGrantColumnHeader = localize('objectManagement.withGrantColumnHeader', "With Grant"); +export const DenyColumnHeader = localize('objectManagement.denyColumnHeader', "Deny"); +export const SelectSecurablesDialogTitle = localize('objectManagement.selectSecurablesDialogTitle', "Select Securables"); +export function ExplicitPermissionsTableLabelSelected(name: string): string { return localize('objectManagement.explicitPermissionsTableLabelSelected', "Explicit permissions for: {0}", name); } +export function EffectivePermissionsTableLabelSelected(name: string): string { return localize('objectManagement.effectivePermissionsTableLabelSelected', "Effective permissions for: {0}", name); } export function RefreshObjectExplorerError(error: string): string { return localize({ @@ -133,12 +146,15 @@ export const LoginNotSelectedError = localize('objectManagement.loginNotSelected export const MembershipSectionHeader = localize('objectManagement.membershipLabel', "Membership"); export const MemberSectionHeader = localize('objectManagement.membersLabel', "Members"); export const SchemaText = localize('objectManagement.schemaLabel', "Schema"); + +// Database export const DatabaseExistsError = (dbName: string) => localize('objectManagement.databaseExistsError', "Database '{0}' already exists. Choose a different database name.", dbName); export const CollationText = localize('objectManagement.collationLabel', "Collation"); export const RecoveryModelText = localize('objectManagement.recoveryModelLabel', "Recovery Model"); export const CompatibilityLevelText = localize('objectManagement.compatibilityLevelLabel', "Compatibility Level"); export const ContainmentTypeText = localize('objectManagement.containmentTypeLabel', "Containment Type"); + // 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?"); export const DeleteLoginConfirmationText: string = localize('objectManagement.deleteLoginConfirmation', "Deleting server logins does not delete the database users associated with the logins. To complete the process, delete the users in each database. It may be necessary to first transfer the ownership of schemas to new users."); diff --git a/extensions/mssql/src/objectManagement/objectManagementService.ts b/extensions/mssql/src/objectManagement/objectManagementService.ts index 85575b2e11..989567a605 100644 --- a/extensions/mssql/src/objectManagement/objectManagementService.ts +++ b/extensions/mssql/src/objectManagement/objectManagementService.ts @@ -67,6 +67,126 @@ export class ObjectManagementService extends BaseService implements IObjectManag } } +const ServerLevelSecurableTypes: ObjectManagement.SecurableTypeMetadata[] = [ + { + name: 'Server', + displayName: 'Server', + permissions: [{ + name: 'CONNECT SQL', + displayName: 'CONNECT SQL' + }, { + name: 'VIEW ANY DATABASE', + displayName: 'VIEW ANY DATABASE' + }] + }, { + name: 'ServerRole', + displayName: 'Server Role', + permissions: [{ + name: 'ALTER', + displayName: 'ALTER' + }, { + name: 'CONTROL', + displayName: 'CONTROL' + }, { + name: 'TAKE OWNERSHIP', + displayName: 'TAKE OWNERSHIP' + }] + } +]; + +const DatabaseLevelSecurableTypes: ObjectManagement.SecurableTypeMetadata[] = [ + { + name: 'AggregateFunction', + displayName: 'Aggregate Function', + permissions: [{ + name: 'EXECUTE', + displayName: 'EXECUTE' + }, { + name: 'ALTER', + displayName: 'ALTER' + }] + }, { + name: 'Table', + displayName: 'Table', + permissions: [{ + name: 'SELECT', + displayName: 'SELECT' + }, { + name: 'ALTER', + displayName: 'ALTER' + }, { + name: 'CONTROL', + displayName: 'CONTROL' + }, { + name: 'TAKE OWNERSHIP', + displayName: 'TAKE OWNERSHIP' + }] + }, { + name: 'View', + displayName: 'View', + permissions: [{ + name: 'ALTER', + displayName: 'ALTER' + }, { + name: 'CONTROL', + displayName: 'CONTROL' + }, { + name: 'TAKE OWNERSHIP', + displayName: 'TAKE OWNERSHIP' + }] + } +] + +const ServerLevelPermissions: ObjectManagement.SecurablePermissions[] = [ + { + name: 'Server', + type: 'Server', + permissions: [ + { + permission: 'CONNECT SQL', + grant: true, + grantor: 'sa', + withGrant: undefined + }, { + permission: 'VIEW ANY DATABASE', + grant: false, + grantor: 'sa', + withGrant: undefined + } + ], + effectivePermissions: ['CONNECT SQL', 'VIEW ANY DATABASE'] + } +]; + +const DatabaseLevelPermissions: ObjectManagement.SecurablePermissions[] = [ + { + name: 'table1', + type: 'Table', + schema: 'dbo', + permissions: [ + { + permission: 'SELECT', + grant: true, + grantor: '', + withGrant: undefined + } + ], + effectivePermissions: ['SELECT'] + }, { + name: 'view1', + type: 'View', + schema: 'Sales', + permissions: [ + { + permission: 'ALTER', + grant: true, + grantor: '', + withGrant: undefined + } + ], + effectivePermissions: ['ALTER'] + } +]; export class TestObjectManagementService implements IObjectManagementService { initializeView(contextId: string, objectType: ObjectManagement.NodeType, connectionUri: string, database: string, isNewObject: boolean, parentUrn: string, objectUrn: string): Thenable> { let obj; @@ -102,18 +222,18 @@ export class TestObjectManagementService implements IObjectManagementService { return this.delayAndResolve(); } - async search(contextId: string, objectTypes: ObjectManagement.NodeType[], searchText: string, schema: string): Promise { + 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)); + items.push(...this.generateSearchResult(type, schema, 15)); }); return this.delayAndResolve(items); } - private generateSearchResult(objectType: ObjectManagement.NodeType, count: number): ObjectManagement.SearchResultItem[] { + private generateSearchResult(objectType: ObjectManagement.NodeType, schema: string | undefined, count: number): ObjectManagement.SearchResultItem[] { let items: ObjectManagement.SearchResultItem[] = []; for (let i = 0; i < count; i++) { - items.push({ name: `${objectType} ${i}`, type: objectType }); + items.push({ name: `${objectType} ${i}`, schema: schema, type: objectType }); } return items; } @@ -136,7 +256,8 @@ export class TestObjectManagementService implements IObjectManagementService { serverRoles: ['public', 'bulkadmin'], connectPermission: true, isEnabled: true, - isLockedOut: false + isLockedOut: false, + securablePermissions: [] }, authenticationTypes: [ObjectManagement.AuthenticationType.Sql, ObjectManagement.AuthenticationType.Windows], supportAdvancedOptions: true, @@ -144,7 +265,8 @@ export class TestObjectManagementService implements IObjectManagementService { canEditLockedOutState: false, languages: languages, databases: databases, - serverRoles: serverRoles + serverRoles: serverRoles, + supportedSecurableTypes: ServerLevelSecurableTypes }; } else { login = { @@ -160,7 +282,8 @@ export class TestObjectManagementService implements IObjectManagementService { connectPermission: true, isEnabled: true, isLockedOut: false, - password: '******************' + password: '******************', + securablePermissions: ServerLevelPermissions }, authenticationTypes: [ObjectManagement.AuthenticationType.Sql, ObjectManagement.AuthenticationType.Windows], supportAdvancedOptions: true, @@ -168,7 +291,8 @@ export class TestObjectManagementService implements IObjectManagementService { canEditLockedOutState: false, languages: languages, databases: databases, - serverRoles: serverRoles + serverRoles: serverRoles, + supportedSecurableTypes: ServerLevelSecurableTypes }; } return login; @@ -192,7 +316,8 @@ export class TestObjectManagementService implements IObjectManagementService { loginName: 'sa', ownedSchemas: [], databaseRoles: [], - password: '' + password: '', + securablePermissions: [] }, languages: languages, schemas: schemas, @@ -203,7 +328,8 @@ export class TestObjectManagementService implements IObjectManagementService { ObjectManagement.UserType.AADAuthentication, ObjectManagement.UserType.SqlAuthentication, ObjectManagement.UserType.NoLoginAccess - ] + ], + supportedSecurableTypes: DatabaseLevelSecurableTypes }; } else { viewInfo = { @@ -214,7 +340,8 @@ export class TestObjectManagementService implements IObjectManagementService { defaultLanguage: '', loginName: 'sa', ownedSchemas: ['dbo'], - databaseRoles: ['dbmanager', 'bulkadmin'] + databaseRoles: ['dbmanager', 'bulkadmin'], + securablePermissions: DatabaseLevelPermissions }, languages: languages, schemas: schemas, @@ -225,7 +352,8 @@ export class TestObjectManagementService implements IObjectManagementService { ObjectManagement.UserType.AADAuthentication, ObjectManagement.UserType.SqlAuthentication, ObjectManagement.UserType.NoLoginAccess - ] + ], + supportedSecurableTypes: DatabaseLevelSecurableTypes }; } return viewInfo; @@ -237,19 +365,23 @@ export class TestObjectManagementService implements IObjectManagementService { name: '', members: [], owner: '', - memberships: [] + memberships: [], + securablePermissions: [] }, isFixedRole: false, serverRoles: ['ServerLevelServerRole 1', 'ServerLevelServerRole 2', 'ServerLevelServerRole 3', 'ServerLevelServerRole 4'], + supportedSecurableTypes: ServerLevelSecurableTypes } : { objectInfo: { name: 'ServerLevelServerRole 1', members: ['ServerLevelLogin 1', 'ServerLevelServerRole 2'], owner: 'ServerLevelLogin 2', - memberships: ['ServerLevelServerRole 3', 'ServerLevelServerRole 4'] + memberships: ['ServerLevelServerRole 3', 'ServerLevelServerRole 4'], + securablePermissions: ServerLevelPermissions }, isFixedRole: false, - serverRoles: ['ServerLevelServerRole 2', 'ServerLevelServerRole 3', 'ServerLevelServerRole 4'] + serverRoles: ['ServerLevelServerRole 2', 'ServerLevelServerRole 3', 'ServerLevelServerRole 4'], + supportedSecurableTypes: ServerLevelSecurableTypes }; } @@ -259,16 +391,20 @@ export class TestObjectManagementService implements IObjectManagementService { name: '', defaultSchema: 'dbo', ownedSchemas: [], + securablePermissions: [] }, - schemas: ['dbo', 'sys', 'admin'] + schemas: ['dbo', 'sys', 'admin'], + supportedSecurableTypes: [] } : { objectInfo: { name: 'app role1', password: '******************', defaultSchema: 'dbo', ownedSchemas: ['dbo'], + securablePermissions: DatabaseLevelPermissions }, - schemas: ['dbo', 'sys', 'admin'] + schemas: ['dbo', 'sys', 'admin'], + supportedSecurableTypes: DatabaseLevelSecurableTypes }; } @@ -278,17 +414,21 @@ export class TestObjectManagementService implements IObjectManagementService { name: '', owner: '', members: [], - ownedSchemas: [] + ownedSchemas: [], + securablePermissions: [] }, - schemas: ['dbo', 'sys', 'admin'] + schemas: ['dbo', 'sys', 'admin'], + supportedSecurableTypes: DatabaseLevelSecurableTypes } : { objectInfo: { name: 'db role1', owner: '', members: [], - ownedSchemas: ['dbo'] + ownedSchemas: ['dbo'], + securablePermissions: DatabaseLevelPermissions }, - schemas: ['dbo', 'sys', 'admin'] + schemas: ['dbo', 'sys', 'admin'], + supportedSecurableTypes: DatabaseLevelSecurableTypes }; } diff --git a/extensions/mssql/src/objectManagement/ui/applicationRoleDialog.ts b/extensions/mssql/src/objectManagement/ui/applicationRoleDialog.ts index 8504cd2efa..7f5a9fc124 100644 --- a/extensions/mssql/src/objectManagement/ui/applicationRoleDialog.ts +++ b/extensions/mssql/src/objectManagement/ui/applicationRoleDialog.ts @@ -3,14 +3,15 @@ * 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 { 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 '../../ui/dialogBase'; +import { DefaultMaxTableRowCount } from '../../ui/dialogBase'; +import { PrincipalDialogBase } from './principalDialogBase'; -export class ApplicationRoleDialog extends ObjectManagementDialogBase { +export class ApplicationRoleDialog extends PrincipalDialogBase { // Sections private generalSection: azdata.GroupContainer; private ownedSchemasSection: azdata.GroupContainer; @@ -25,7 +26,7 @@ export class ApplicationRoleDialog extends ObjectManagementDialogBase { + protected override async initializeUI(): Promise { + await super.initializeUI(); this.initializeGeneralSection(); this.initializeOwnedSchemasSection(); - this.formContainer.addItems([this.generalSection, this.ownedSchemasSection]); + this.formContainer.addItems([this.generalSection, this.ownedSchemasSection, this.securableSection], this.getSectionItemLayout()); } private initializeGeneralSection(): void { @@ -84,7 +86,7 @@ export class ApplicationRoleDialog extends ObjectManagementDialogBase { // It is not allowed to have unassigned schema. return this.objectInfo.ownedSchemas.indexOf(item) === -1; diff --git a/extensions/mssql/src/objectManagement/ui/databaseRoleDialog.ts b/extensions/mssql/src/objectManagement/ui/databaseRoleDialog.ts index 2926a42a8c..2ba4f046a2 100644 --- a/extensions/mssql/src/objectManagement/ui/databaseRoleDialog.ts +++ b/extensions/mssql/src/objectManagement/ui/databaseRoleDialog.ts @@ -3,14 +3,15 @@ * 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 { 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 '../../ui/dialogBase'; +import { DefaultMaxTableRowCount } from '../../ui/dialogBase'; +import { PrincipalDialogBase } from './principalDialogBase'; -export class DatabaseRoleDialog extends ObjectManagementDialogBase { +export class DatabaseRoleDialog extends PrincipalDialogBase { // Sections private generalSection: azdata.GroupContainer; private ownedSchemasSection: azdata.GroupContainer; @@ -27,18 +28,19 @@ export class DatabaseRoleDialog extends ObjectManagementDialogBase { + protected override async initializeUI(): Promise { + await super.initializeUI(); this.initializeGeneralSection(); this.initializeOwnedSchemasSection(); this.initializeMemberSection(); - this.formContainer.addItems([this.generalSection, this.ownedSchemasSection, this.memberSection]); + this.formContainer.addItems([this.generalSection, this.ownedSchemasSection, this.memberSection, this.securableSection], this.getSectionItemLayout()); } private initializeGeneralSection(): void { @@ -53,9 +55,11 @@ export class DatabaseRoleDialog extends ObjectManagementDialogBase { const dialog = new FindObjectDialog(this.objectManagementService, { objectTypes: [ObjectManagement.NodeType.ApplicationRole, ObjectManagement.NodeType.DatabaseRole, ObjectManagement.NodeType.User], + selectAllObjectTypes: true, multiSelect: false, contextId: this.contextId, - title: localizedConstants.SelectDatabaseRoleOwnerDialogTitle + title: localizedConstants.SelectDatabaseRoleOwnerDialogTitle, + showSchemaColumn: false }); await dialog.open(); const result = await dialog.waitForClose(); @@ -70,48 +74,45 @@ export class DatabaseRoleDialog extends ObjectManagementDialogBase [m])); + this.memberTable = this.createTable(localizedConstants.MemberSectionHeader, [localizedConstants.NameText], this.objectInfo.members.map(m => [m])); const buttonContainer = this.addButtonsForTable(this.memberTable, localizedConstants.AddMemberAriaLabel, localizedConstants.RemoveMemberAriaLabel, async () => { const dialog = new FindObjectDialog(this.objectManagementService, { objectTypes: [ObjectManagement.NodeType.DatabaseRole, ObjectManagement.NodeType.User], + selectAllObjectTypes: true, multiSelect: true, contextId: this.contextId, - title: localizedConstants.SelectDatabaseRoleMemberDialogTitle + title: localizedConstants.SelectDatabaseRoleMemberDialogTitle, + showSchemaColumn: false }); await dialog.open(); const result = await dialog.waitForClose(); - this.addMembers(result.selectedObjects.map(r => r.name)); + await this.addMembers(result.selectedObjects.map(r => r.name)); }, async () => { if (this.memberTable.selectedRows.length === 1) { - this.removeMember(this.memberTable.selectedRows[0]); + await this.removeMember(this.memberTable.selectedRows[0]); } }); this.memberSection = this.createGroup(localizedConstants.MemberSectionHeader, [this.memberTable, buttonContainer]); } - private addMembers(names: string[]): void { + private async addMembers(names: string[]): Promise { names.forEach(n => { if (this.objectInfo.members.indexOf(n) === -1) { this.objectInfo.members.push(n); } }); - this.updateMembersTable(); + await this.updateMembersTable(); } - private removeMember(idx: number): void { + private async removeMember(idx: number): Promise { this.objectInfo.members.splice(idx, 1); - this.updateMembersTable(); + await this.updateMembersTable(); } - private updateMembersTable(): void { - this.setTableData(this.memberTable, this.objectInfo.members.map(m => [m])); + private async updateMembersTable(): Promise { + await this.setTableData(this.memberTable, this.objectInfo.members.map(m => [m])); this.onFormFieldChange(); } @@ -120,7 +121,7 @@ export class DatabaseRoleDialog extends ObjectManagementDialogBase { // It is not allowed to have unassigned schema. return this.objectInfo.ownedSchemas.indexOf(item) === -1; diff --git a/extensions/mssql/src/objectManagement/ui/findObjectDialog.ts b/extensions/mssql/src/objectManagement/ui/findObjectDialog.ts index d928fa6f82..72295183f5 100644 --- a/extensions/mssql/src/objectManagement/ui/findObjectDialog.ts +++ b/extensions/mssql/src/objectManagement/ui/findObjectDialog.ts @@ -5,15 +5,19 @@ import * as azdata from 'azdata'; import * as mssql from 'mssql'; -import { DefaultTableListItemEnabledStateGetter, DefaultMaxTableHeight, DialogBase, TableListItemComparer, TableListItemValueGetter } from '../../ui/dialogBase'; +import { DefaultTableListItemEnabledStateGetter, DefaultMaxTableRowCount, DialogBase, TableListItemComparer } from '../../ui/dialogBase'; import * as localizedConstants from '../localizedConstants'; import { getErrorMessage } from '../../utils'; +type ObjectType = string | { name: string, displayName: string }; + export interface FindObjectDialogOptions { - objectTypes: mssql.ObjectManagement.NodeType[]; + objectTypes: ObjectType[]; + selectAllObjectTypes: boolean; multiSelect: boolean; contextId: string; title: string; + showSchemaColumn?: boolean; } export interface FindObjectDialogResult { @@ -22,15 +26,10 @@ export interface FindObjectDialogResult { const ObjectComparer: TableListItemComparer = (item1, item2) => { - return item1.name === item2.name && item1.type === item2.type; + return item1.name === item2.name && item1.type === item2.type && item1.schema === item2.schema; }; -const ObjectRowValueGetter: TableListItemValueGetter = - (item) => { - return [item.name, localizedConstants.getNodeTypeDisplayName(item.type, true)]; - }; - -const ObjectsTableMaxHeight = 700; +const ObjectsTableMaxRowCount = 20; export class FindObjectDialog extends DialogBase { private objectTypesTable: azdata.TableComponent; @@ -38,7 +37,7 @@ export class FindObjectDialog extends DialogBase { private objectsTable: azdata.TableComponent; private objectsLoadingComponent: azdata.LoadingComponent; private result: FindObjectDialogResult; - private selectedObjectTypes: string[] = []; + private selectedObjectTypes: ObjectType[] = []; private allObjects: mssql.ObjectManagement.SearchResultItem[] = []; constructor(private readonly objectManagementService: mssql.IObjectManagementService, private readonly options: FindObjectDialogOptions) { @@ -47,40 +46,52 @@ export class FindObjectDialog extends DialogBase { this.result = { selectedObjects: [] }; - this.selectedObjectTypes = [...options.objectTypes]; + this.selectedObjectTypes = options.selectAllObjectTypes ? [...options.objectTypes] : []; + } + + private getObjectTypeName(objectType: ObjectType): string { + return typeof objectType === 'string' ? objectType : objectType.name; + } + + private getObjectTypeDisplayName(objectType: ObjectType): string { + return typeof objectType === 'string' ? localizedConstants.getNodeTypeDisplayName(objectType, true) : objectType.displayName; } protected override async initialize(): Promise { this.dialogObject.okButton.enabled = false; - this.objectTypesTable = this.createTableList(localizedConstants.ObjectTypeText, + this.objectTypesTable = this.createTableList(localizedConstants.ObjectTypeText, [localizedConstants.ObjectTypeText], this.options.objectTypes, this.selectedObjectTypes, - DefaultMaxTableHeight, + DefaultMaxTableRowCount, DefaultTableListItemEnabledStateGetter, (item) => { - return [localizedConstants.getNodeTypeDisplayName(item, true)]; + return [this.getObjectTypeDisplayName(item)]; + }, (item1, item2) => { + return this.getObjectTypeName(item1) === this.getObjectTypeName(item2); }); this.findButton = this.createButton(localizedConstants.FindText, localizedConstants.FindText, async () => { await this.onFindObjectButtonClick(); - }); + }, this.options.selectAllObjectTypes); const buttonContainer = this.createButtonContainer([this.findButton]); const objectTypeSection = this.createGroup(localizedConstants.ObjectTypeText, [this.objectTypesTable, buttonContainer]); + const columns = [localizedConstants.NameText, localizedConstants.ObjectTypeText]; + if (this.options.showSchemaColumn) { + columns.splice(1, 0, localizedConstants.SchemaText); + } if (this.options.multiSelect) { this.objectsTable = this.createTableList(localizedConstants.ObjectsText, - [localizedConstants.NameText, localizedConstants.ObjectTypeText], + columns, this.allObjects, this.result.selectedObjects, - ObjectsTableMaxHeight, + ObjectsTableMaxRowCount, DefaultTableListItemEnabledStateGetter, - ObjectRowValueGetter, + (item) => { + return this.getObjectRowValue(item); + }, ObjectComparer); } else { - this.objectsTable = this.createTable(localizedConstants.ObjectsText, [{ - value: localizedConstants.NameText, - }, { - value: localizedConstants.ObjectTypeText - }], []); + this.objectsTable = this.createTable(localizedConstants.ObjectsText, columns, [], ObjectsTableMaxRowCount); this.disposables.push(this.objectsTable.onRowSelected(async () => { if (this.objectsTable.selectedRows.length > 0) { this.result.selectedObjects = [this.allObjects[this.objectsTable.selectedRows[0]]]; @@ -95,7 +106,7 @@ export class FindObjectDialog extends DialogBase { }).component(); const objectsSection = this.createGroup(localizedConstants.ObjectsText, [this.objectsLoadingComponent]); - this.formContainer.addItems([objectTypeSection, objectsSection]); + this.formContainer.addItems([objectTypeSection, objectsSection], this.getSectionItemLayout()); } protected override get dialogResult(): FindObjectDialogResult | undefined { @@ -107,16 +118,18 @@ export class FindObjectDialog extends DialogBase { this.objectsLoadingComponent.loading = true; this.findButton.enabled = false; try { - const results = await this.objectManagementService.search(this.options.contextId, this.selectedObjectTypes); + const results = await this.objectManagementService.search(this.options.contextId, this.selectedObjectTypes.map(item => this.getObjectTypeName(item))); 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); + data = this.getDataForTableList(this.allObjects, this.result.selectedObjects, DefaultTableListItemEnabledStateGetter, (item) => { + return this.getObjectRowValue(item); + }, ObjectComparer); } else { - data = this.allObjects.map(item => ObjectRowValueGetter(item)); + data = this.allObjects.map(item => { return this.getObjectRowValue(item); }); } - this.setTableData(this.objectsTable, data, ObjectsTableMaxHeight); + await this.setTableData(this.objectsTable, data, ObjectsTableMaxRowCount); this.objectsLoadingComponent.loadingCompletedText = localizedConstants.LoadingObjectsCompletedText(results.length); } catch (err) { this.dialogObject.message = { @@ -132,4 +145,12 @@ export class FindObjectDialog extends DialogBase { this.findButton.enabled = this.selectedObjectTypes.length > 0; this.dialogObject.okButton.enabled = this.result.selectedObjects.length > 0; } + + private getObjectRowValue(item: mssql.ObjectManagement.SearchResultItem): string[] { + const row = [item.name, this.getObjectTypeName(item.type)]; + if (this.options.showSchemaColumn) { + row.splice(1, 0, item.schema); + } + return row; + } } diff --git a/extensions/mssql/src/objectManagement/ui/loginDialog.ts b/extensions/mssql/src/objectManagement/ui/loginDialog.ts index b770ef5348..3513b000bb 100644 --- a/extensions/mssql/src/objectManagement/ui/loginDialog.ts +++ b/extensions/mssql/src/objectManagement/ui/loginDialog.ts @@ -4,15 +4,16 @@ *--------------------------------------------------------------------------------------------*/ import * as azdata from 'azdata'; import * as vscode from 'vscode'; -import { ObjectManagementDialogBase, ObjectManagementDialogOptions } from './objectManagementDialogBase'; +import { ObjectManagementDialogOptions } from './objectManagementDialogBase'; import { IObjectManagementService, ObjectManagement } from 'mssql'; import * as objectManagementLoc from '../localizedConstants'; import * as uiLoc from '../../ui/localizedConstants'; import { AlterLoginDocUrl, CreateLoginDocUrl, PublicServerRoleName } from '../constants'; import { isValidSQLPassword } from '../utils'; -import { DefaultMaxTableHeight } from '../../ui/dialogBase'; +import { DefaultMaxTableRowCount } from '../../ui/dialogBase'; +import { PrincipalDialogBase } from './principalDialogBase'; -export class LoginDialog extends ObjectManagementDialogBase { +export class LoginDialog extends PrincipalDialogBase { private generalSection: azdata.GroupContainer; private sqlAuthSection: azdata.GroupContainer; private serverRoleSection: azdata.GroupContainer; @@ -34,7 +35,7 @@ export class LoginDialog extends ObjectManagementDialogBase { + protected override async initializeUI(): Promise { + await super.initializeUI(); const sections: azdata.Component[] = []; this.initializeGeneralSection(); sections.push(this.generalSection); @@ -94,12 +96,13 @@ export class LoginDialog extends ObjectManagementDialogBase { return item !== PublicServerRoleName }); @@ -220,7 +223,7 @@ export class LoginDialog extends ObjectManagementDialogBase> extends ObjectManagementDialogBase { + protected securableTable: azdata.TableComponent; + protected permissionTable: azdata.TableComponent; + protected effectivePermissionTable: azdata.TableComponent; + protected securableSection: azdata.GroupContainer; + protected explicitPermissionTableLabel: azdata.TextComponent; + protected effectivePermissionTableLabel: azdata.TextComponent; + private securablePermissions: mssql.ObjectManagement.SecurablePermissions[] = []; + + constructor(objectManagementService: mssql.IObjectManagementService, options: ObjectManagementDialogOptions, private readonly showSchemaColumn: boolean, private readonly supportEffectivePermissions: boolean = true) { + super(objectManagementService, options); + } + + protected override async initializeUI(): Promise { + this.securablePermissions = deepClone(this.objectInfo.securablePermissions); + this.initializeSecurableSection(); + } + + private initializeSecurableSection(): void { + const items: azdata.Component[] = []; + const securableTableColumns = [localizedConstants.NameText, localizedConstants.ObjectTypeText]; + if (this.showSchemaColumn) { + securableTableColumns.splice(1, 0, localizedConstants.SchemaText); + } + this.securableTable = this.createTable(localizedConstants.SecurablesText, securableTableColumns, this.getSecurableTableData()); + const buttonContainer = this.addButtonsForTable(this.securableTable, localizedConstants.AddSecurableAriaLabel, localizedConstants.RemoveSecurableAriaLabel, + () => this.onAddSecurableButtonClicked(), () => this.onRemoveSecurableButtonClicked()); + this.disposables.push(this.securableTable.onRowSelected(async () => { + await this.updatePermissionsTable(); + })); + this.explicitPermissionTableLabel = this.modelView.modelBuilder.text().withProps({ value: localizedConstants.ExplicitPermissionsTableLabel }).component(); + this.permissionTable = this.modelView.modelBuilder.table().withProps({ + ariaLabel: localizedConstants.ExplicitPermissionsTableLabel, + columns: + [{ + type: azdata.ColumnType.text, + value: localizedConstants.PermissionColumnHeader + }, { + type: azdata.ColumnType.text, + value: localizedConstants.GrantorColumnHeader + }, { + type: azdata.ColumnType.checkBox, + value: localizedConstants.GrantColumnHeader + }, { + type: azdata.ColumnType.checkBox, + value: localizedConstants.WithGrantColumnHeader + }, { + type: azdata.ColumnType.checkBox, + value: localizedConstants.DenyColumnHeader + }], + data: [], + height: getTableHeight(0), + width: DefaultTableWidth + }).component(); + this.disposables.push(this.permissionTable.onCellAction(async (arg: azdata.ICheckboxCellActionEventArgs) => { + const permissionName = this.permissionTable.data[arg.row][0]; + const securable = this.securablePermissions[this.securableTable.selectedRows[0]]; + let permission: mssql.ObjectManagement.SecurablePermissionItem = securable.permissions.find(securablePermission => securablePermission.permission === permissionName); + if (!permission) { + permission = { + permission: permissionName, + grantor: '' + }; + securable.permissions.push(permission); + } + if (arg.column === GrantColumnIndex) { + permission.grant = arg.checked ? true : undefined; + if (!arg.checked) { + permission.withGrant = undefined; + } + } else if (arg.column === WithGrantColumnIndex) { + permission.withGrant = arg.checked ? true : undefined; + if (arg.checked) { + permission.grant = true; + } + } else if (arg.column === DenyColumnIndex) { + permission.grant = arg.checked ? false : undefined; + if (arg.checked) { + permission.withGrant = undefined; + } + } + await this.updatePermissionsTable(); + this.updateSecurablePermissions(); + // Restore the focus to previously selected cell. + this.permissionTable.setActiveCell(arg.row, arg.column); + })); + + items.push(this.securableTable, buttonContainer, this.explicitPermissionTableLabel, this.permissionTable); + if (this.showEffectivePermissions) { + this.effectivePermissionTableLabel = this.modelView.modelBuilder.text().withProps({ value: localizedConstants.EffectivePermissionsTableLabel }).component(); + this.effectivePermissionTable = this.createTable(localizedConstants.EffectivePermissionsTableLabel, [localizedConstants.PermissionColumnHeader], []); + items.push(this.effectivePermissionTableLabel, this.effectivePermissionTable); + } + this.securableSection = this.createGroup(localizedConstants.SecurablesText, items, true, true); + } + + private async onAddSecurableButtonClicked(): Promise { + const dialog = new FindObjectDialog(this.objectManagementService, { + objectTypes: this.viewInfo.supportedSecurableTypes, + selectAllObjectTypes: false, + multiSelect: true, + contextId: this.contextId, + title: localizedConstants.SelectSecurablesDialogTitle, + showSchemaColumn: this.showSchemaColumn + }); + await dialog.open(); + const result = await dialog.waitForClose(); + if (result && result.selectedObjects.length > 0) { + result.selectedObjects.forEach(obj => { + if (this.securablePermissions.find(securable => securable.type === obj.type && securable.name === obj.name && securable.schema === obj.schema)) { + return; + } + const securableTypeMetadata = this.viewInfo.supportedSecurableTypes.find(securableType => securableType.name === obj.type); + this.securablePermissions.push({ + name: obj.name, + schema: obj.schema, + type: obj.type, + permissions: securableTypeMetadata.permissions.map(permission => { + return { + permission: permission.name, + grantor: '', + grant: undefined, + withGrant: undefined + }; + }), + effectivePermissions: [] + }); + }); + const data = this.getSecurableTableData(); + await this.setTableData(this.securableTable, data); + } + } + + private async onRemoveSecurableButtonClicked(): Promise { + if (this.securableTable.selectedRows.length === 1) { + this.securablePermissions.splice(this.securableTable.selectedRows[0], 1); + const data = this.getSecurableTableData(); + await this.setTableData(this.securableTable, data); + this.updateSecurablePermissions(); + } + } + + private getSecurableTableData(): string[][] { + return this.securablePermissions.map(securable => { + const row = [securable.name, this.getSecurableTypeDisplayName(securable.type)]; + if (this.showSchemaColumn) { + row.splice(1, 0, securable.schema); + } + return row; + }); + } + + private async updatePermissionsTable(): Promise { + let permissionsTableData: any[][] = []; + let effectivePermissionsTableData: any[][] = []; + let explicitPermissionsLabel = localizedConstants.ExplicitPermissionsTableLabel; + let effectivePermissionsLabel = localizedConstants.EffectivePermissionsTableLabel; + if (this.securableTable.selectedRows.length === 1) { + const securable = this.securablePermissions[this.securableTable.selectedRows[0]]; + if (securable) { + const securableDisplayName = securable.schema ? `${securable.schema}.${securable.name}` : securable.name; + explicitPermissionsLabel = localizedConstants.ExplicitPermissionsTableLabelSelected(securableDisplayName); + effectivePermissionsLabel = localizedConstants.EffectivePermissionsTableLabelSelected(securableDisplayName); + const securableTypeMetadata = this.viewInfo.supportedSecurableTypes.find(securableType => securableType.name === securable.type); + permissionsTableData = securable.permissions.map(permission => { + return [permission.permission, permission.grantor, { checked: permission.grant === true }, { checked: permission.withGrant === true }, { checked: permission.grant === false }]; + }); + permissionsTableData = securableTypeMetadata.permissions.map(permissionMetadata => { + const permission = securable.permissions.find(securablePermission => securablePermission.permission === permissionMetadata.name); + return [ + permissionMetadata.name, + permission?.grantor ?? '', + { checked: permission?.grant === true }, + { checked: permission?.withGrant === true }, + { checked: permission?.grant === false }]; + }); + effectivePermissionsTableData = securable.effectivePermissions.map(permission => [permission]); + } + } + this.explicitPermissionTableLabel.value = explicitPermissionsLabel; + await this.setTableData(this.permissionTable, permissionsTableData); + if (this.showEffectivePermissions) { + this.effectivePermissionTableLabel.value = effectivePermissionsLabel; + await this.setTableData(this.effectivePermissionTable, effectivePermissionsTableData); + } + } + + private updateSecurablePermissions(): void { + // Only save securable permissions that have grant or deny value. + this.objectInfo.securablePermissions = deepClone(this.securablePermissions.filter((securablePermissions) => { + return securablePermissions.permissions.some(permission => permission.grant !== undefined); + })); + this.objectInfo.securablePermissions.forEach(securablePermissions => { + securablePermissions.permissions = securablePermissions.permissions.filter(permission => permission.grant !== undefined); + }); + this.onFormFieldChange(); + } + + private getSecurableTypeDisplayName(securableType: string): string { + const securableTypeMetadata = this.viewInfo.supportedSecurableTypes.find(securableTypeMetadata => securableTypeMetadata.name === securableType); + return securableTypeMetadata ? securableTypeMetadata.displayName : securableType; + } + + private get showEffectivePermissions(): boolean { + return !this.options.isNewObject && this.supportEffectivePermissions; + } +} diff --git a/extensions/mssql/src/objectManagement/ui/serverRoleDialog.ts b/extensions/mssql/src/objectManagement/ui/serverRoleDialog.ts index 68299741ec..de2be177e9 100644 --- a/extensions/mssql/src/objectManagement/ui/serverRoleDialog.ts +++ b/extensions/mssql/src/objectManagement/ui/serverRoleDialog.ts @@ -3,13 +3,14 @@ * 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 { ObjectManagementDialogOptions } from './objectManagementDialogBase'; import { IObjectManagementService, ObjectManagement } from 'mssql'; import * as localizedConstants from '../localizedConstants'; import { AlterServerRoleDocUrl, CreateServerRoleDocUrl } from '../constants'; import { FindObjectDialog } from './findObjectDialog'; +import { PrincipalDialogBase } from './principalDialogBase'; -export class ServerRoleDialog extends ObjectManagementDialogBase { +export class ServerRoleDialog extends PrincipalDialogBase { // Sections private generalSection: azdata.GroupContainer; private membershipSection: azdata.GroupContainer; @@ -27,22 +28,24 @@ export class ServerRoleDialog extends ObjectManagementDialogBase { + protected override async initializeUI(): Promise { + await super.initializeUI(); this.initializeGeneralSection(); this.initializeMemberSection(); const sections: azdata.Component[] = [this.generalSection, this.memberSection]; if (!this.viewInfo.isFixedRole) { this.initializeMembershipSection(); sections.push(this.membershipSection); + sections.push(this.securableSection); } - this.formContainer.addItems(sections); + this.formContainer.addItems(sections, this.getSectionItemLayout()); } private initializeGeneralSection(): void { @@ -57,6 +60,7 @@ export class ServerRoleDialog extends ObjectManagementDialogBase { const dialog = new FindObjectDialog(this.objectManagementService, { objectTypes: [ObjectManagement.NodeType.ServerLevelLogin, ObjectManagement.NodeType.ServerLevelServerRole], + selectAllObjectTypes: true, multiSelect: false, contextId: this.contextId, title: localizedConstants.SelectServerRoleOwnerDialogTitle @@ -76,48 +80,44 @@ export class ServerRoleDialog extends ObjectManagementDialogBase [m])); + this.memberTable = this.createTable(localizedConstants.MemberSectionHeader, [localizedConstants.NameText], this.objectInfo.members.map(m => [m])); const buttonContainer = this.addButtonsForTable(this.memberTable, localizedConstants.AddMemberAriaLabel, localizedConstants.RemoveMemberAriaLabel, async () => { const dialog = new FindObjectDialog(this.objectManagementService, { objectTypes: [ObjectManagement.NodeType.ServerLevelLogin, ObjectManagement.NodeType.ServerLevelServerRole], + selectAllObjectTypes: true, multiSelect: true, contextId: this.contextId, title: localizedConstants.SelectServerRoleMemberDialogTitle }); await dialog.open(); const result = await dialog.waitForClose(); - this.addMembers(result.selectedObjects.map(r => r.name)); + await this.addMembers(result.selectedObjects.map(r => r.name)); }, async () => { if (this.memberTable.selectedRows.length === 1) { - this.removeMember(this.memberTable.selectedRows[0]); + await this.removeMember(this.memberTable.selectedRows[0]); } }); this.memberSection = this.createGroup(localizedConstants.MemberSectionHeader, [this.memberTable, buttonContainer]); } - private addMembers(names: string[]): void { + private async addMembers(names: string[]): Promise { names.forEach(n => { if (this.objectInfo.members.indexOf(n) === -1) { this.objectInfo.members.push(n); } }); - this.updateMembersTable(); + await this.updateMembersTable(); } - private removeMember(idx: number): void { + private async removeMember(idx: number): Promise { this.objectInfo.members.splice(idx, 1); - this.updateMembersTable(); + await this.updateMembersTable(); } - private updateMembersTable(): void { - this.setTableData(this.memberTable, this.objectInfo.members.map(m => [m])); + private async updateMembersTable(): Promise { + await this.setTableData(this.memberTable, this.objectInfo.members.map(m => [m])); this.onFormFieldChange(); } diff --git a/extensions/mssql/src/objectManagement/ui/userDialog.ts b/extensions/mssql/src/objectManagement/ui/userDialog.ts index f9675c4594..cf69e10252 100644 --- a/extensions/mssql/src/objectManagement/ui/userDialog.ts +++ b/extensions/mssql/src/objectManagement/ui/userDialog.ts @@ -3,14 +3,15 @@ * 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 { ObjectManagementDialogOptions } from './objectManagementDialogBase'; import { IObjectManagementService, ObjectManagement } from 'mssql'; import * as localizedConstants from '../localizedConstants'; import { AlterUserDocUrl, CreateUserDocUrl } from '../constants'; import { isValidSQLPassword } from '../utils'; -import { DefaultMaxTableHeight } from '../../ui/dialogBase'; +import { DefaultMaxTableRowCount } from '../../ui/dialogBase'; +import { PrincipalDialogBase } from './principalDialogBase'; -export class UserDialog extends ObjectManagementDialogBase { +export class UserDialog extends PrincipalDialogBase { private generalSection: azdata.GroupContainer; private ownedSchemaSection: azdata.GroupContainer; private membershipSection: azdata.GroupContainer; @@ -31,7 +32,7 @@ export class UserDialog extends ObjectManagementDialogBase { + protected override async initializeUI(): Promise { + await super.initializeUI(); this.initializeGeneralSection(); this.initializeOwnedSchemaSection(); this.initializeMembershipSection(); this.initializeAdvancedSection(); - this.formContainer.addItems([this.generalSection, this.ownedSchemaSection, this.membershipSection, this.advancedSection]); + this.formContainer.addItems([this.generalSection, this.ownedSchemaSection, this.membershipSection, this.securableSection, this.advancedSection], this.getSectionItemLayout()); setTimeout(() => { this.setViewByUserType(); }, 100); @@ -94,7 +96,7 @@ export class UserDialog extends ObjectManagementDialogBase { this.objectInfo.loginName = newValue; - }, this.viewInfo.logins, this.objectInfo.loginName, this.options.isNewObject); + }, this.options.isNewObject ? this.viewInfo.logins : [this.objectInfo.loginName], this.objectInfo.loginName, this.options.isNewObject); this.loginContainer = this.createLabelInputContainer(localizedConstants.LoginText, this.loginDropdown); this.passwordInput = this.createPasswordInputBox(localizedConstants.PasswordText, async (newValue) => { @@ -119,7 +121,7 @@ export class UserDialog extends ObjectManagementDialogBase { // It is not allowed to have unassigned schema. return this.objectInfo.ownedSchemas.indexOf(item) === -1; @@ -157,6 +159,11 @@ export class UserDialog extends ObjectManagementDialogBase = (item: T) => boolean; @@ -168,7 +168,7 @@ export abstract class DialogBase { columnNames: string[], allItems: T[], selectedItems: T[], - maxHeight: number = DefaultMaxTableHeight, + maxRowCount: number = DefaultMaxTableRowCount, enabledStateGetter: TableListItemEnabledStateGetter = DefaultTableListItemEnabledStateGetter, rowValueGetter: TableListItemValueGetter = DefaultTableListItemValueGetter, itemComparer: TableListItemComparer = DefaultTableListItemComparer): azdata.TableComponent { @@ -179,7 +179,7 @@ export abstract class DialogBase { data: data, columns: [ { - value: uiLoc.SelectedText, + value: uiLoc.SelectText, type: azdata.ColumnType.checkBox, options: { actionOnCheckbox: azdata.ActionOnCellCheckboxCheck.customAction } }, ...columnNames.map(name => { @@ -187,7 +187,7 @@ export abstract class DialogBase { }) ], width: DefaultTableWidth, - height: getTableHeight(data.length, DefaultMinTableRowCount, maxHeight) + height: getTableHeight(data.length, DefaultMinTableRowCount, maxRowCount) } ).component(); this.disposables.push(table.onCellAction!((arg: azdata.ICheckboxCellActionEventArgs) => { @@ -203,9 +203,11 @@ export abstract class DialogBase { return table; } - protected setTableData(table: azdata.TableComponent, data: any[][], maxHeight: number = DefaultMaxTableHeight) { - table.data = data; - table.height = getTableHeight(data.length, DefaultMinTableRowCount, maxHeight); + protected async setTableData(table: azdata.TableComponent, data: any[][], maxRowCount: number = DefaultMaxTableRowCount) { + await table.updateProperties({ + data: data, + height: getTableHeight(data.length, DefaultMinTableRowCount, maxRowCount) + }); } protected getDataForTableList( @@ -221,14 +223,14 @@ export abstract class DialogBase { }); } - protected createTable(ariaLabel: string, columns: azdata.TableColumn[], data: any[][], maxHeight: number = DefaultMaxTableHeight): azdata.TableComponent { + protected createTable(ariaLabel: string, columns: string[], data: any[][], maxRowCount: number = DefaultMaxTableRowCount): azdata.TableComponent { const table = this.modelView.modelBuilder.table().withProps( { ariaLabel: ariaLabel, data: data, columns: columns, width: DefaultTableWidth, - height: getTableHeight(data.length, DefaultMinTableRowCount, maxHeight) + height: getTableHeight(data.length, DefaultMinTableRowCount, maxRowCount) } ).component(); return table; @@ -238,7 +240,8 @@ export abstract class DialogBase { let addButton: azdata.ButtonComponent; let removeButton: azdata.ButtonComponent; const updateButtons = () => { - removeButton.enabled = table.selectedRows.length > 0; + this.onFormFieldChange(); + removeButton.enabled = table.selectedRows?.length === 1 && table.selectedRows[0] !== -1 && table.selectedRows[0] < table.data.length; } addButton = this.createButton(uiLoc.AddText, addButtonAriaLabel, async () => { await addHandler(); @@ -246,6 +249,9 @@ export abstract class DialogBase { }); removeButton = this.createButton(uiLoc.RemoveText, removeButtonAriaLabel, async () => { await removeHandler(); + if (table.selectedRows.length === 1 && table.selectedRows[0] >= table.data.length) { + table.selectedRows = [table.data.length - 1]; + } updateButtons(); }, false); this.disposables.push(table.onRowSelected(() => { @@ -308,12 +314,12 @@ export abstract class DialogBase { } } - protected addItem(container: azdata.DivContainer | azdata.FlexContainer, item: azdata.Component, index?: number): void { + protected addItem(container: azdata.DivContainer | azdata.FlexContainer, item: azdata.Component, itemLayout?: azdata.FlexItemLayout, index?: number): void { if (container.items.indexOf(item) === -1) { if (index === undefined) { - container.addItem(item); + container.addItem(item, itemLayout); } else { - container.insertItem(item, index); + container.insertItem(item, index, itemLayout); } } } @@ -327,6 +333,10 @@ export abstract class DialogBase { 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(); + }).withItems(items, this.getSectionItemLayout()).component(); + } + + protected getSectionItemLayout(): azdata.FlexItemLayout { + return { CSSStyles: { 'margin-block-end': '5px' } }; } } diff --git a/extensions/mssql/src/ui/localizedConstants.ts b/extensions/mssql/src/ui/localizedConstants.ts index 3fa0a32c93..c72e132939 100644 --- a/extensions/mssql/src/ui/localizedConstants.ts +++ b/extensions/mssql/src/ui/localizedConstants.ts @@ -11,7 +11,7 @@ export const YesText: string = localize('mssql.ui.yesText', "Yes"); export const OkText: string = localize('mssql.ui.OkText', "OK"); export const LoadingDialogText: string = localize('mssql.ui.loadingDialog', "Loading dialog..."); export const ScriptText: string = localize('mssql.ui.scriptText', "Script"); -export const SelectedText = localize('objectManagement.selectedLabel', "Selected"); +export const SelectText = localize('objectManagement.selectLabel', "Select"); export const AddText = localize('objectManagement.addText', "Add…"); export const RemoveText = localize('objectManagement.removeText', "Remove"); export const NoActionScriptedMessage: string = localize('mssql.ui.noActionScriptedMessage', "There is no action to be scripted."); diff --git a/extensions/schema-compare/src/test/testContext.ts b/extensions/schema-compare/src/test/testContext.ts index 4b6438b433..30c3b12809 100644 --- a/extensions/schema-compare/src/test/testContext.ts +++ b/extensions/schema-compare/src/test/testContext.ts @@ -295,7 +295,8 @@ export function createViewContext(): ViewTestContext { columns: [] as string[], onRowSelected: onClick.event, onCellAction: onClick.event, - appendData: (_data: any[][]) => undefined + appendData: (_data: any[][]) => undefined, + setActiveCell: (_row: number, _column: number) => undefined }); let tableBuilder: azdata.ComponentBuilder = { component: () => table(), diff --git a/src/sql/azdata.proposed.d.ts b/src/sql/azdata.proposed.d.ts index 3537ce5638..3c8741351b 100644 --- a/src/sql/azdata.proposed.d.ts +++ b/src/sql/azdata.proposed.d.ts @@ -1947,4 +1947,11 @@ declare module 'azdata' { isPrimary: boolean; } } + + export interface TableComponent { + /** + * Set active cell. + */ + setActiveCell(row: number, column: number): void; + } } diff --git a/src/sql/platform/dashboard/browser/interfaces.ts b/src/sql/platform/dashboard/browser/interfaces.ts index d4aa019a4a..16a7fb8da8 100644 --- a/src/sql/platform/dashboard/browser/interfaces.ts +++ b/src/sql/platform/dashboard/browser/interfaces.ts @@ -32,7 +32,8 @@ export enum ComponentEventType { export enum ModelViewAction { SelectTab = 'selectTab', AppendData = 'appendData', - Filter = 'filter' + Filter = 'filter', + SetActiveCell = 'setActiveCell' } /** diff --git a/src/sql/workbench/api/common/extHostModelView.ts b/src/sql/workbench/api/common/extHostModelView.ts index 93f736a520..a50ce71daf 100644 --- a/src/sql/workbench/api/common/extHostModelView.ts +++ b/src/sql/workbench/api/common/extHostModelView.ts @@ -1503,6 +1503,10 @@ class TableComponentWrapper extends ComponentWrapper implements azdata.TableComp public appendData(v: any[][]): Thenable { return this.doAction(ModelViewAction.AppendData, v); } + + public setActiveCell(row: number, column: number): void { + this.doAction(ModelViewAction.SetActiveCell, row, column); + } } class DropDownWrapper extends ComponentWrapper implements azdata.DropDownComponent { diff --git a/src/sql/workbench/api/common/sqlExtHostTypes.ts b/src/sql/workbench/api/common/sqlExtHostTypes.ts index e147813cb2..9f60cf66b3 100644 --- a/src/sql/workbench/api/common/sqlExtHostTypes.ts +++ b/src/sql/workbench/api/common/sqlExtHostTypes.ts @@ -186,7 +186,8 @@ export enum ModelComponentTypes { export enum ModelViewAction { SelectTab = 'selectTab', AppendData = 'appendData', - Filter = 'filter' + Filter = 'filter', + SetActiveCell = 'setActiveCell' } export enum ColumnSizingMode { diff --git a/src/sql/workbench/browser/modelComponents/table.component.ts b/src/sql/workbench/browser/modelComponents/table.component.ts index f6d61ae105..cd40ff1c45 100644 --- a/src/sql/workbench/browser/modelComponents/table.component.ts +++ b/src/sql/workbench/browser/modelComponents/table.component.ts @@ -40,6 +40,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { ITableService } from 'sql/workbench/services/table/browser/tableService'; +import { deepClone, equals } from 'vs/base/common/objects'; export enum ColumnSizingMode { ForceFit = 0, // all columns will be sized to fit in viewable space, no horiz scroll bar @@ -358,24 +359,27 @@ export default class TableComponent extends ComponentBase { this.registerPlugins(columnName, column); }) + Object.keys(this._buttonColumns).forEach(col => this.registerPlugins(col, this._buttonColumns[col])); + Object.keys(this._hyperlinkColumns).forEach(col => this.registerPlugins(col, this._hyperlinkColumns[col])); + Object.keys(this._contextMenuColumns).forEach(col => this.registerPlugins(col, this._contextMenuColumns[col])); + + this._table.columns = this._tableColumns; + this._table.autosizeColumns(); + } this._table.setData(this._tableData); this._table.setTableTitle(this.title); if (this.selectedRows) { this._table.setSelectedRows(this.selectedRows); } - this._checkboxColumns.forEach((column, columnName) => { - this.registerPlugins(columnName, column); - }) - Object.keys(this._buttonColumns).forEach(col => this.registerPlugins(col, this._buttonColumns[col])); - Object.keys(this._hyperlinkColumns).forEach(col => this.registerPlugins(col, this._hyperlinkColumns[col])); - Object.keys(this._contextMenuColumns).forEach(col => this.registerPlugins(col, this._contextMenuColumns[col])); - if (this.headerFilter === true) { this.registerFilterPlugin(); this._tableData.clearFilter(); @@ -433,7 +437,8 @@ export default class TableComponent extends ComponentBase { @@ -589,7 +594,6 @@ export default class TableComponent extends ComponentBase | ButtonColumn<{}> | HyperlinkColumn<{}> | ContextMenuColumn<{}>): void { - const index = 'index' in plugin ? plugin.index : this.columns?.findIndex(x => x === col || ('value' in x && x['value'] === col)); if (index >= 0) { this._tableColumns.splice(index, 0, plugin.definition); @@ -598,10 +602,6 @@ export default class TableComponent extends ComponentBase