From f4d5ab616ca3b5f9de6a7f48e099feb2506e8835 Mon Sep 17 00:00:00 2001 From: Alan Ren Date: Tue, 30 May 2023 09:53:44 -0700 Subject: [PATCH] add objects dialog (#23243) --- extensions/mssql/src/mssql.d.ts | 28 +++-- .../objectManagement/localizedConstants.ts | 13 ++- .../ui/applicationRoleDialog.ts | 2 +- .../objectManagement/ui/databaseRoleDialog.ts | 2 +- .../objectManagement/ui/findObjectDialog.ts | 15 ++- .../src/objectManagement/ui/loginDialog.ts | 2 +- .../ui/objectSelectionMethodDialog.ts | 101 ++++++++++++++++++ .../ui/principalDialogBase.ts | 88 +++++++++++---- .../objectManagement/ui/serverRoleDialog.ts | 2 +- .../src/objectManagement/ui/userDialog.ts | 2 +- extensions/mssql/src/ui/dialogBase.ts | 32 ++++-- .../mssql/src/ui/scriptableDialogBase.ts | 8 +- .../groupContainer.component.ts | 8 +- .../modelComponents/media/groupLayout.css | 7 ++ 14 files changed, 250 insertions(+), 60 deletions(-) create mode 100644 extensions/mssql/src/objectManagement/ui/objectSelectionMethodDialog.ts diff --git a/extensions/mssql/src/mssql.d.ts b/extensions/mssql/src/mssql.d.ts index 1bb257213d..4bedc2d715 100644 --- a/extensions/mssql/src/mssql.d.ts +++ b/extensions/mssql/src/mssql.d.ts @@ -975,6 +975,16 @@ declare module 'mssql' { supportedSecurableTypes: SecurableTypeMetadata[]; } + /** + * Base interface for database level security principal object's view information. + */ + export interface DatabaseLevelPrincipalViewInfo extends SecurityPrincipalViewInfo { + /** + * The schemas in the database. + */ + schemas: string[]; + } + /** * Server level login. */ @@ -1237,7 +1247,7 @@ declare module 'mssql' { /** * The information required to render the user view. */ - export interface UserViewInfo extends SecurityPrincipalViewInfo { + export interface UserViewInfo extends DatabaseLevelPrincipalViewInfo { /** * All user types supported by the database. */ @@ -1246,10 +1256,6 @@ declare module 'mssql' { * All languages supported by the database. */ languages: string[]; - /** - * All schemas in the database. - */ - schemas: string[]; /** * Name of all the logins in the server. */ @@ -1313,11 +1319,7 @@ declare module 'mssql' { /** * Interface representing the information required to render the application role view. */ - export interface ApplicationRoleViewInfo extends SecurityPrincipalViewInfo { - /** - * List of all the schemas in the database. - */ - schemas: string[]; + export interface ApplicationRoleViewInfo extends DatabaseLevelPrincipalViewInfo { } /** @@ -1341,11 +1343,7 @@ declare module 'mssql' { /** * Interface representing the information required to render the database role view. */ - export interface DatabaseRoleViewInfo extends SecurityPrincipalViewInfo { - /** - * List of all the schemas in the database. - */ - schemas: string[]; + export interface DatabaseRoleViewInfo extends DatabaseLevelPrincipalViewInfo { } /** diff --git a/extensions/mssql/src/objectManagement/localizedConstants.ts b/extensions/mssql/src/objectManagement/localizedConstants.ts index 051b8c3863..1a7e551577 100644 --- a/extensions/mssql/src/objectManagement/localizedConstants.ts +++ b/extensions/mssql/src/objectManagement/localizedConstants.ts @@ -196,8 +196,10 @@ export const SelectServerRoleMemberDialogTitle = localize('objectManagement.serv export const SelectServerRoleOwnerDialogTitle = localize('objectManagement.serverRole.SelectOwnerDialogTitle', "Select Server Role Owner"); // Find Object Dialog +export const ObjectTypesText = localize('objectManagement.objectTypesLabel', "Object Types"); +export const FilterSectionTitle = localize('objectManagement.filterSectionTitle', "Filters"); export const ObjectTypeText = localize('objectManagement.objectTypeLabel', "Object Type"); -export const FilterText = localize('objectManagement.filterText', "Filter"); +export const SearchTextLabel = localize('objectManagement.SearchTextLabel', "Search Text"); export const FindText = localize('objectManagement.findText', "Find"); export const SelectText = localize('objectManagement.selectText', "Select"); export const ObjectsText = localize('objectManagement.objectsLabel', "Objects"); @@ -206,8 +208,15 @@ export function LoadingObjectsCompletedText(count: number): string { return localize('objectManagement.loadingObjectsCompletedLabel', "Loading objects completed, {0} objects found", count); } -// Util functions +// ObjectSelectionMethodDialog +export const ObjectSelectionMethodDialogTitle = localize('objectManagement.objectSelectionMethodDialogTitle', "Add Objects"); +export const ObjectSelectionMethodDialog_TypeLabel = localize('objectManagement.ObjectSelectionMethodDialog_TypeLabel', "How do you want to add objects?"); +export const ObjectSelectionMethodDialog_SpecificObjects = localize('objectManagement.ObjectSelectionMethodDialog_SpecificObjects', "Specific objects…"); +export const ObjectSelectionMethodDialog_AllObjectsOfTypes = localize('objectManagement.ObjectSelectionMethodDialog_AllObjectsOfTypes', "All objects of certain types"); +export const ObjectSelectionMethodDialog_AllObjectsOfSchema = localize('objectManagement.ObjectSelectionMethodDialog_AllObjectsOfSchema', "All objects belonging to a schema"); +export const ObjectSelectionMethodDialog_SelectSchemaDropdownLabel = localize('objectManagement.ObjectSelectionMethodDialog_SelectSchemaDropdownLabel', "Schema"); +// Util functions export function getNodeTypeDisplayName(type: string, inTitle: boolean = false): string { switch (type) { case ObjectManagement.NodeType.ApplicationRole: diff --git a/extensions/mssql/src/objectManagement/ui/applicationRoleDialog.ts b/extensions/mssql/src/objectManagement/ui/applicationRoleDialog.ts index 7f5a9fc124..b7eff521e3 100644 --- a/extensions/mssql/src/objectManagement/ui/applicationRoleDialog.ts +++ b/extensions/mssql/src/objectManagement/ui/applicationRoleDialog.ts @@ -26,7 +26,7 @@ export class ApplicationRoleDialog extends PrincipalDialogBase { private objectTypesTable: azdata.TableComponent; + private searchTextInputBox: azdata.InputBoxComponent; private findButton: azdata.ButtonComponent; private objectsTable: azdata.TableComponent; private objectsLoadingComponent: azdata.LoadingComponent; @@ -54,7 +55,7 @@ export class FindObjectDialog extends DialogBase { protected override async initialize(): Promise { this.dialogObject.okButton.enabled = false; - this.objectTypesTable = this.createTableList(localizedConstants.ObjectTypeText, + this.objectTypesTable = this.createTableList(localizedConstants.ObjectTypesText, [localizedConstants.ObjectTypeText], this.options.objectTypes, this.selectedObjectTypes, @@ -64,11 +65,17 @@ export class FindObjectDialog extends DialogBase { }, (item1, item2) => { return item1.name === item2.name; }); + this.searchTextInputBox = this.createInputBox(localizedConstants.SearchTextLabel, async () => { }); + const searchTextRow = this.createLabelInputContainer(localizedConstants.SearchTextLabel, this.searchTextInputBox); 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 filterSection = this.createGroup(localizedConstants.FilterSectionTitle, [ + searchTextRow, + this.objectTypesTable, + buttonContainer + ]); const columns = [localizedConstants.NameText, localizedConstants.ObjectTypeText]; if (this.options.showSchemaColumn) { columns.splice(1, 0, localizedConstants.SchemaText); @@ -101,7 +108,7 @@ export class FindObjectDialog extends DialogBase { }).component(); const objectsSection = this.createGroup(localizedConstants.ObjectsText, [this.objectsLoadingComponent]); - this.formContainer.addItems([objectTypeSection, objectsSection], this.getSectionItemLayout()); + this.formContainer.addItems([filterSection, objectsSection], this.getSectionItemLayout()); } protected override get dialogResult(): FindObjectDialogResult | undefined { @@ -113,7 +120,7 @@ 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.map(item => item.name)); + const results = await this.objectManagementService.search(this.options.contextId, this.selectedObjectTypes.map(item => item.name), this.searchTextInputBox.value); this.allObjects.splice(0, this.allObjects.length, ...results); let data; if (this.options.multiSelect) { diff --git a/extensions/mssql/src/objectManagement/ui/loginDialog.ts b/extensions/mssql/src/objectManagement/ui/loginDialog.ts index 3513b000bb..f396b93eb3 100644 --- a/extensions/mssql/src/objectManagement/ui/loginDialog.ts +++ b/extensions/mssql/src/objectManagement/ui/loginDialog.ts @@ -35,7 +35,7 @@ export class LoginDialog extends PrincipalDialogBase { + private specificObjectsRadioButton: azdata.RadioButtonComponent; + private allObjectsOfTypesRadioButton: azdata.RadioButtonComponent; + private allObjectsOfSchemaRadioButton: azdata.RadioButtonComponent; + private objectTypesTable: azdata.TableComponent; + private schemaRow: azdata.FlexContainer; + private result: ObjectSelectionMethodDialogResult; + + constructor(private readonly options: ObjectSelectionMethodDialogOptions) { + super(localizedConstants.ObjectSelectionMethodDialogTitle, 'ObjectSelectionMethodDialog'); + this.result = { + method: ObjectSelectionMethod.SpecificObjects, + schema: undefined, + objectTypes: [] + }; + } + + protected override async initialize(): Promise { + const radioGroupName = 'objectSelectionMethodRadioGroup'; + this.specificObjectsRadioButton = this.createRadioButton(localizedConstants.ObjectSelectionMethodDialog_SpecificObjects, radioGroupName, true, async (checked) => { await this.handleTypeChange(checked); }); + this.allObjectsOfTypesRadioButton = this.createRadioButton(localizedConstants.ObjectSelectionMethodDialog_AllObjectsOfTypes, radioGroupName, false, async (checked) => { await this.handleTypeChange(checked); }); + this.allObjectsOfSchemaRadioButton = this.createRadioButton(localizedConstants.ObjectSelectionMethodDialog_AllObjectsOfSchema, radioGroupName, false, async (checked) => { await this.handleTypeChange(checked); }); + + const typeGroup = this.createGroup(localizedConstants.ObjectSelectionMethodDialog_TypeLabel, [this.specificObjectsRadioButton, this.allObjectsOfTypesRadioButton, this.allObjectsOfSchemaRadioButton], false); + this.objectTypesTable = this.createTableList(localizedConstants.ObjectTypeText, + [localizedConstants.ObjectTypesText], + this.options.objectTypes, + this.result.objectTypes, + DefaultMaxTableRowCount, + DefaultTableListItemEnabledStateGetter, (item) => { + return [item.displayName]; + }, (item1, item2) => { + return item1.name === item2.name; + }); + + const schemaDropdown = this.createDropdown(localizedConstants.ObjectSelectionMethodDialog_SelectSchemaDropdownLabel, async (newValue) => { + this.result.schema = newValue; + }, this.options.schemas, this.options.schemas[0]); + this.schemaRow = this.createLabelInputContainer(localizedConstants.ObjectSelectionMethodDialog_SelectSchemaDropdownLabel, schemaDropdown); + await this.setComponentsVisibility(false, false); + this.formContainer.addItems([typeGroup, this.schemaRow, this.objectTypesTable], this.getSectionItemLayout()); + } + + private async handleTypeChange(checked: boolean): Promise { + let method: ObjectSelectionMethod = ObjectSelectionMethod.SpecificObjects; + let showSchema = false; + let showObjectTypes = false; + await this.setComponentsVisibility(showObjectTypes, showSchema); + if (this.allObjectsOfTypesRadioButton.checked) { + method = ObjectSelectionMethod.AllObjectsOfTypes; + showSchema = false; + showObjectTypes = true; + } else if (this.allObjectsOfSchemaRadioButton.checked) { + method = ObjectSelectionMethod.AllObjectsOfSchema; + showSchema = true; + showObjectTypes = false; + } + this.result.method = method; + await this.setComponentsVisibility(showObjectTypes, showSchema); + } + + private async setComponentsVisibility(showObjectTypes: boolean, showSchema: boolean): Promise { + await this.schemaRow.updateCssStyles({ display: showSchema ? 'flex' : 'none' }); + await this.objectTypesTable.updateCssStyles({ display: showObjectTypes ? 'block' : 'none' }); + } + + protected override get dialogResult(): ObjectSelectionMethodDialogResult | undefined { + return this.result; + } + + protected override async onFormFieldChange(): Promise { + this.dialogObject.okButton.enabled = this.result.method !== ObjectSelectionMethod.AllObjectsOfTypes || this.result.objectTypes.length > 0; + } +} diff --git a/extensions/mssql/src/objectManagement/ui/principalDialogBase.ts b/extensions/mssql/src/objectManagement/ui/principalDialogBase.ts index e0ca56b888..853d589753 100644 --- a/extensions/mssql/src/objectManagement/ui/principalDialogBase.ts +++ b/extensions/mssql/src/objectManagement/ui/principalDialogBase.ts @@ -8,14 +8,20 @@ import * as mssql from 'mssql'; import * as localizedConstants from '../localizedConstants'; import { ObjectManagementDialogBase, ObjectManagementDialogOptions } from './objectManagementDialogBase'; -import { FindObjectDialog } from './findObjectDialog'; +import { FindObjectDialog, FindObjectDialogResult } from './findObjectDialog'; import { deepClone } from '../../util/objects'; import { DefaultTableWidth, getTableHeight } from '../../ui/dialogBase'; +import { ObjectSelectionMethod, ObjectSelectionMethodDialog } from './objectSelectionMethodDialog'; const GrantColumnIndex = 2; const WithGrantColumnIndex = 3; const DenyColumnIndex = 4; +export interface PrincipalDialogOptions extends ObjectManagementDialogOptions { + isDatabaseLevelPrincipal: boolean; + supportEffectivePermissions: boolean; +} + /** * Base class for security principal dialogs such as user, role, etc. */ @@ -28,8 +34,8 @@ export abstract class PrincipalDialogBase { @@ -40,12 +46,12 @@ export abstract class PrincipalDialogBase this.onAddSecurableButtonClicked(), () => this.onRemoveSecurableButtonClicked()); + (button) => this.onAddSecurableButtonClicked(button), () => this.onRemoveSecurableButtonClicked()); this.disposables.push(this.securableTable.onRowSelected(async () => { await this.updatePermissionsTable(); })); @@ -115,19 +121,40 @@ export abstract class PrincipalDialogBase { - 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 => { + private async onAddSecurableButtonClicked(button: azdata.ButtonComponent): Promise { + const selectedObjects: mssql.ObjectManagement.SearchResultItem[] = []; + if (this.dialogOptions.isDatabaseLevelPrincipal) { + const methodDialog = new ObjectSelectionMethodDialog({ + objectTypes: this.viewInfo.supportedSecurableTypes, + schemas: (>this.viewInfo).schemas, + }); + await methodDialog.open(); + const methodResult = await methodDialog.waitForClose(); + if (methodResult) { + switch (methodResult.method) { + case ObjectSelectionMethod.AllObjectsOfTypes: + selectedObjects.push(... await this.searchForObjects(methodResult.objectTypes.map(item => item.name))); + break; + case ObjectSelectionMethod.AllObjectsOfSchema: + selectedObjects.push(... await this.searchForObjects(this.viewInfo.supportedSecurableTypes.map(item => item.name), methodResult.schema)); + break; + default: + const objectsResult = await this.openFindObjectsDialog(); + if (objectsResult) { + selectedObjects.push(...objectsResult.selectedObjects); + } + break; + } + } + } else { + const result = await this.openFindObjectsDialog(); + if (result) { + selectedObjects.push(...result.selectedObjects); + } + } + + if (selectedObjects.length > 0) { + selectedObjects.forEach(obj => { if (this.securablePermissions.find(securable => securable.type === obj.type && securable.name === obj.name && securable.schema === obj.schema)) { return; } @@ -150,6 +177,27 @@ export abstract class PrincipalDialogBase { + this.updateLoadingStatus(true); + const result = await this.objectManagementService.search(this.contextId, objectTypes, undefined, schema); + this.updateLoadingStatus(false); + return result; + } + + private async openFindObjectsDialog(): Promise { + const dialog = new FindObjectDialog(this.objectManagementService, { + objectTypes: this.viewInfo.supportedSecurableTypes, + selectAllObjectTypes: false, + multiSelect: true, + contextId: this.contextId, + title: localizedConstants.SelectSecurablesDialogTitle, + showSchemaColumn: this.dialogOptions.isDatabaseLevelPrincipal + }); + await dialog.open(); + return await dialog.waitForClose(); } private async onRemoveSecurableButtonClicked(): Promise { @@ -164,7 +212,7 @@ export abstract class PrincipalDialogBase { const row = [securable.name, this.getSecurableTypeDisplayName(securable.type)]; - if (this.showSchemaColumn) { + if (this.dialogOptions.isDatabaseLevelPrincipal) { row.splice(1, 0, securable.schema); } return row; @@ -223,6 +271,6 @@ export abstract class PrincipalDialogBase { public async open(): Promise { try { - this.onLoadingStatusChanged(true); + this.updateLoadingStatus(true); const initializeDialogPromise = new Promise((async resolve => { this.dialogObject.registerContent(async view => { this._modelView = view; @@ -97,7 +97,7 @@ export abstract class DialogBase { azdata.window.openDialog(this.dialogObject); await initializeDialogPromise; await this.initialize(); - this.onLoadingStatusChanged(false); + this.updateLoadingStatus(false); } catch (err) { azdata.window.closeDialog(this.dialogObject); throw err; @@ -121,10 +121,10 @@ export abstract class DialogBase { 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(); + protected createLabelInputContainer(label: string, component: azdata.Component, required: boolean = false): azdata.FlexContainer { + const labelComponent = this.modelView.modelBuilder.text().withProps({ width: DefaultLabelWidth, value: label, requiredIndicator: 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' }); + container.addItem(component, { flex: '1 1 auto' }); return container; } @@ -236,7 +236,7 @@ export abstract class DialogBase { return table; } - protected addButtonsForTable(table: azdata.TableComponent, addButtonAriaLabel: string, removeButtonAriaLabel: string, addHandler: () => Promise, removeHandler: () => Promise): azdata.FlexContainer { + protected addButtonsForTable(table: azdata.TableComponent, addButtonAriaLabel: string, removeButtonAriaLabel: string, addHandler: (button: azdata.ButtonComponent) => Promise, removeHandler: (button: azdata.ButtonComponent) => Promise): azdata.FlexContainer { let addButton: azdata.ButtonComponent; let removeButton: azdata.ButtonComponent; const updateButtons = () => { @@ -244,11 +244,11 @@ export abstract class DialogBase { 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(); + await addHandler(addButton); updateButtons(); }); removeButton = this.createButton(uiLoc.RemoveText, removeButtonAriaLabel, async () => { - await removeHandler(); + await removeHandler(removeButton); if (table.selectedRows.length === 1 && table.selectedRows[0] >= table.data.length) { table.selectedRows = [table.data.length - 1]; } @@ -308,6 +308,20 @@ export abstract class DialogBase { }).withItems(items, { flex: '0 0 auto' }).component(); } + protected createRadioButton(label: string, groupName: string, checked: boolean, handler: (checked: boolean) => Promise): azdata.RadioButtonComponent { + const radio = this.modelView.modelBuilder.radioButton().withProps({ + label: label, + name: groupName, + checked: checked + }).component(); + this.disposables.push(radio.onDidChangeCheckedState(async checked => { + await handler(checked); + this.onFormFieldChange(); + await this.runValidation(false); + })); + return radio; + } + protected removeItem(container: azdata.DivContainer | azdata.FlexContainer, item: azdata.Component): void { if (container.items.indexOf(item) !== -1) { container.removeItem(item); @@ -324,7 +338,7 @@ export abstract class DialogBase { } } - protected onLoadingStatusChanged(isLoading: boolean): void { + protected updateLoadingStatus(isLoading: boolean): void { if (this._loadingComponent) { this._loadingComponent.loading = isLoading; } diff --git a/extensions/mssql/src/ui/scriptableDialogBase.ts b/extensions/mssql/src/ui/scriptableDialogBase.ts index 9bce48e5fc..924d9c4a7a 100644 --- a/extensions/mssql/src/ui/scriptableDialogBase.ts +++ b/extensions/mssql/src/ui/scriptableDialogBase.ts @@ -68,8 +68,8 @@ export abstract class ScriptableDialogBase; private async onScriptButtonClick(): Promise { - this.onLoadingStatusChanged(true); + this.updateLoadingStatus(true); try { const isValid = await this.runValidation(); if (!isValid) { @@ -104,7 +104,7 @@ export abstract class ScriptableDialogBase - {{_containerLayout.header}} + {{header}} +
@@ -37,6 +38,7 @@ import { ILogService } from 'vs/platform/log/common/log';
+
` }) export default class GroupContainer extends ContainerBase implements IComponent, OnDestroy, AfterViewInit { @@ -95,6 +97,10 @@ export default class GroupContainer extends ContainerBase((props) => props.collapsed, false); } + public get header(): string { + return this._containerLayout?.header; + } + private hasHeader(): boolean { return this._containerLayout && !!this._containerLayout.header; } diff --git a/src/sql/workbench/browser/modelComponents/media/groupLayout.css b/src/sql/workbench/browser/modelComponents/media/groupLayout.css index f785cdf17e..71c312ffb8 100644 --- a/src/sql/workbench/browser/modelComponents/media/groupLayout.css +++ b/src/sql/workbench/browser/modelComponents/media/groupLayout.css @@ -9,6 +9,13 @@ box-sizing: border-box; } +.modelview-group-fieldset { + border: none; + margin-inline: 0px; + padding-inline: 0px; + padding-block: 0px; +} + .modelview-group-row { display: table-row; }