add objects dialog (#23243)

This commit is contained in:
Alan Ren
2023-05-30 09:53:44 -07:00
committed by GitHub
parent 82f5ef7ea3
commit f4d5ab616c
14 changed files with 250 additions and 60 deletions

View File

@@ -975,6 +975,16 @@ declare module 'mssql' {
supportedSecurableTypes: SecurableTypeMetadata[]; supportedSecurableTypes: SecurableTypeMetadata[];
} }
/**
* Base interface for database level security principal object's view information.
*/
export interface DatabaseLevelPrincipalViewInfo<T extends SecurityPrincipalObject> extends SecurityPrincipalViewInfo<T> {
/**
* The schemas in the database.
*/
schemas: string[];
}
/** /**
* Server level login. * Server level login.
*/ */
@@ -1237,7 +1247,7 @@ declare module 'mssql' {
/** /**
* The information required to render the user view. * The information required to render the user view.
*/ */
export interface UserViewInfo extends SecurityPrincipalViewInfo<User> { export interface UserViewInfo extends DatabaseLevelPrincipalViewInfo<User> {
/** /**
* All user types supported by the database. * All user types supported by the database.
*/ */
@@ -1246,10 +1256,6 @@ declare module 'mssql' {
* All languages supported by the database. * All languages supported by the database.
*/ */
languages: string[]; languages: string[];
/**
* All schemas in the database.
*/
schemas: string[];
/** /**
* Name of all the logins in the server. * 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. * Interface representing the information required to render the application role view.
*/ */
export interface ApplicationRoleViewInfo extends SecurityPrincipalViewInfo<ApplicationRoleInfo> { export interface ApplicationRoleViewInfo extends DatabaseLevelPrincipalViewInfo<ApplicationRoleInfo> {
/**
* List of all the schemas in the database.
*/
schemas: string[];
} }
/** /**
@@ -1341,11 +1343,7 @@ declare module 'mssql' {
/** /**
* Interface representing the information required to render the database role view. * Interface representing the information required to render the database role view.
*/ */
export interface DatabaseRoleViewInfo extends SecurityPrincipalViewInfo<DatabaseRoleInfo> { export interface DatabaseRoleViewInfo extends DatabaseLevelPrincipalViewInfo<DatabaseRoleInfo> {
/**
* List of all the schemas in the database.
*/
schemas: string[];
} }
/** /**

View File

@@ -196,8 +196,10 @@ export const SelectServerRoleMemberDialogTitle = localize('objectManagement.serv
export const SelectServerRoleOwnerDialogTitle = localize('objectManagement.serverRole.SelectOwnerDialogTitle', "Select Server Role Owner"); export const SelectServerRoleOwnerDialogTitle = localize('objectManagement.serverRole.SelectOwnerDialogTitle', "Select Server Role Owner");
// Find Object Dialog // 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 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 FindText = localize('objectManagement.findText', "Find");
export const SelectText = localize('objectManagement.selectText', "Select"); export const SelectText = localize('objectManagement.selectText', "Select");
export const ObjectsText = localize('objectManagement.objectsLabel', "Objects"); 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); 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 { export function getNodeTypeDisplayName(type: string, inTitle: boolean = false): string {
switch (type) { switch (type) {
case ObjectManagement.NodeType.ApplicationRole: case ObjectManagement.NodeType.ApplicationRole:

View File

@@ -26,7 +26,7 @@ export class ApplicationRoleDialog extends PrincipalDialogBase<ObjectManagement.
private ownedSchemaTable: azdata.TableComponent; private ownedSchemaTable: azdata.TableComponent;
constructor(objectManagementService: IObjectManagementService, options: ObjectManagementDialogOptions) { constructor(objectManagementService: IObjectManagementService, options: ObjectManagementDialogOptions) {
super(objectManagementService, options, true, false); super(objectManagementService, { ...options, isDatabaseLevelPrincipal: true, supportEffectivePermissions: false });
} }
protected override postInitializeData(): void { protected override postInitializeData(): void {

View File

@@ -28,7 +28,7 @@ export class DatabaseRoleDialog extends PrincipalDialogBase<ObjectManagement.Dat
private memberTable: azdata.TableComponent; private memberTable: azdata.TableComponent;
constructor(objectManagementService: IObjectManagementService, options: ObjectManagementDialogOptions) { constructor(objectManagementService: IObjectManagementService, options: ObjectManagementDialogOptions) {
super(objectManagementService, options, true, false); super(objectManagementService, { ...options, isDatabaseLevelPrincipal: true, supportEffectivePermissions: false });
} }
protected override get helpUrl(): string { protected override get helpUrl(): string {

View File

@@ -36,6 +36,7 @@ const ObjectsTableMaxRowCount = 20;
export class FindObjectDialog extends DialogBase<FindObjectDialogResult> { export class FindObjectDialog extends DialogBase<FindObjectDialogResult> {
private objectTypesTable: azdata.TableComponent; private objectTypesTable: azdata.TableComponent;
private searchTextInputBox: azdata.InputBoxComponent;
private findButton: azdata.ButtonComponent; private findButton: azdata.ButtonComponent;
private objectsTable: azdata.TableComponent; private objectsTable: azdata.TableComponent;
private objectsLoadingComponent: azdata.LoadingComponent; private objectsLoadingComponent: azdata.LoadingComponent;
@@ -54,7 +55,7 @@ export class FindObjectDialog extends DialogBase<FindObjectDialogResult> {
protected override async initialize(): Promise<void> { protected override async initialize(): Promise<void> {
this.dialogObject.okButton.enabled = false; this.dialogObject.okButton.enabled = false;
this.objectTypesTable = this.createTableList<ObjectTypeInfo>(localizedConstants.ObjectTypeText, this.objectTypesTable = this.createTableList<ObjectTypeInfo>(localizedConstants.ObjectTypesText,
[localizedConstants.ObjectTypeText], [localizedConstants.ObjectTypeText],
this.options.objectTypes, this.options.objectTypes,
this.selectedObjectTypes, this.selectedObjectTypes,
@@ -64,11 +65,17 @@ export class FindObjectDialog extends DialogBase<FindObjectDialogResult> {
}, (item1, item2) => { }, (item1, item2) => {
return item1.name === item2.name; 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 () => { this.findButton = this.createButton(localizedConstants.FindText, localizedConstants.FindText, async () => {
await this.onFindObjectButtonClick(); await this.onFindObjectButtonClick();
}, this.options.selectAllObjectTypes); }, this.options.selectAllObjectTypes);
const buttonContainer = this.createButtonContainer([this.findButton]); 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]; const columns = [localizedConstants.NameText, localizedConstants.ObjectTypeText];
if (this.options.showSchemaColumn) { if (this.options.showSchemaColumn) {
columns.splice(1, 0, localizedConstants.SchemaText); columns.splice(1, 0, localizedConstants.SchemaText);
@@ -101,7 +108,7 @@ export class FindObjectDialog extends DialogBase<FindObjectDialogResult> {
}).component(); }).component();
const objectsSection = this.createGroup(localizedConstants.ObjectsText, [this.objectsLoadingComponent]); 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 { protected override get dialogResult(): FindObjectDialogResult | undefined {
@@ -113,7 +120,7 @@ export class FindObjectDialog extends DialogBase<FindObjectDialogResult> {
this.objectsLoadingComponent.loading = true; this.objectsLoadingComponent.loading = true;
this.findButton.enabled = false; this.findButton.enabled = false;
try { 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); this.allObjects.splice(0, this.allObjects.length, ...results);
let data; let data;
if (this.options.multiSelect) { if (this.options.multiSelect) {

View File

@@ -35,7 +35,7 @@ export class LoginDialog extends PrincipalDialogBase<ObjectManagement.Login, Obj
private lockedOutCheckbox: azdata.CheckBoxComponent; private lockedOutCheckbox: azdata.CheckBoxComponent;
constructor(objectManagementService: IObjectManagementService, options: ObjectManagementDialogOptions) { constructor(objectManagementService: IObjectManagementService, options: ObjectManagementDialogOptions) {
super(objectManagementService, options, false); super(objectManagementService, { ...options, isDatabaseLevelPrincipal: false, supportEffectivePermissions: true });
} }
protected override get helpUrl(): string { protected override get helpUrl(): string {

View File

@@ -0,0 +1,101 @@
/*---------------------------------------------------------------------------------------------
* 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 { DefaultMaxTableRowCount, DefaultTableListItemEnabledStateGetter, DialogBase } from '../../ui/dialogBase';
import * as localizedConstants from '../localizedConstants';
import { ObjectTypeInfo } from './findObjectDialog';
export enum ObjectSelectionMethod {
SpecificObjects,
AllObjectsOfTypes,
AllObjectsOfSchema
}
export interface ObjectSelectionMethodDialogOptions {
objectTypes: ObjectTypeInfo[];
schemas: string[];
}
export interface ObjectSelectionMethodDialogResult {
method: ObjectSelectionMethod;
schema: string | undefined;
objectTypes: ObjectTypeInfo[];
}
export class ObjectSelectionMethodDialog extends DialogBase<ObjectSelectionMethodDialogResult> {
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<void> {
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<ObjectTypeInfo>(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<void> {
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<void> {
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<void> {
this.dialogObject.okButton.enabled = this.result.method !== ObjectSelectionMethod.AllObjectsOfTypes || this.result.objectTypes.length > 0;
}
}

View File

@@ -8,14 +8,20 @@ import * as mssql from 'mssql';
import * as localizedConstants from '../localizedConstants'; import * as localizedConstants from '../localizedConstants';
import { ObjectManagementDialogBase, ObjectManagementDialogOptions } from './objectManagementDialogBase'; import { ObjectManagementDialogBase, ObjectManagementDialogOptions } from './objectManagementDialogBase';
import { FindObjectDialog } from './findObjectDialog'; import { FindObjectDialog, FindObjectDialogResult } from './findObjectDialog';
import { deepClone } from '../../util/objects'; import { deepClone } from '../../util/objects';
import { DefaultTableWidth, getTableHeight } from '../../ui/dialogBase'; import { DefaultTableWidth, getTableHeight } from '../../ui/dialogBase';
import { ObjectSelectionMethod, ObjectSelectionMethodDialog } from './objectSelectionMethodDialog';
const GrantColumnIndex = 2; const GrantColumnIndex = 2;
const WithGrantColumnIndex = 3; const WithGrantColumnIndex = 3;
const DenyColumnIndex = 4; const DenyColumnIndex = 4;
export interface PrincipalDialogOptions extends ObjectManagementDialogOptions {
isDatabaseLevelPrincipal: boolean;
supportEffectivePermissions: boolean;
}
/** /**
* Base class for security principal dialogs such as user, role, etc. * Base class for security principal dialogs such as user, role, etc.
*/ */
@@ -28,8 +34,8 @@ export abstract class PrincipalDialogBase<ObjectInfoType extends mssql.ObjectMan
protected effectivePermissionTableLabel: azdata.TextComponent; protected effectivePermissionTableLabel: azdata.TextComponent;
private securablePermissions: mssql.ObjectManagement.SecurablePermissions[] = []; private securablePermissions: mssql.ObjectManagement.SecurablePermissions[] = [];
constructor(objectManagementService: mssql.IObjectManagementService, options: ObjectManagementDialogOptions, private readonly showSchemaColumn: boolean, private readonly supportEffectivePermissions: boolean = true) { constructor(objectManagementService: mssql.IObjectManagementService, private readonly dialogOptions: PrincipalDialogOptions) {
super(objectManagementService, options); super(objectManagementService, dialogOptions);
} }
protected override async initializeUI(): Promise<void> { protected override async initializeUI(): Promise<void> {
@@ -40,12 +46,12 @@ export abstract class PrincipalDialogBase<ObjectInfoType extends mssql.ObjectMan
private initializeSecurableSection(): void { private initializeSecurableSection(): void {
const items: azdata.Component[] = []; const items: azdata.Component[] = [];
const securableTableColumns = [localizedConstants.NameText, localizedConstants.ObjectTypeText]; const securableTableColumns = [localizedConstants.NameText, localizedConstants.ObjectTypeText];
if (this.showSchemaColumn) { if (this.dialogOptions.isDatabaseLevelPrincipal) {
securableTableColumns.splice(1, 0, localizedConstants.SchemaText); securableTableColumns.splice(1, 0, localizedConstants.SchemaText);
} }
this.securableTable = this.createTable(localizedConstants.SecurablesText, securableTableColumns, this.getSecurableTableData()); this.securableTable = this.createTable(localizedConstants.SecurablesText, securableTableColumns, this.getSecurableTableData());
const buttonContainer = this.addButtonsForTable(this.securableTable, localizedConstants.AddSecurableAriaLabel, localizedConstants.RemoveSecurableAriaLabel, const buttonContainer = this.addButtonsForTable(this.securableTable, localizedConstants.AddSecurableAriaLabel, localizedConstants.RemoveSecurableAriaLabel,
() => this.onAddSecurableButtonClicked(), () => this.onRemoveSecurableButtonClicked()); (button) => this.onAddSecurableButtonClicked(button), () => this.onRemoveSecurableButtonClicked());
this.disposables.push(this.securableTable.onRowSelected(async () => { this.disposables.push(this.securableTable.onRowSelected(async () => {
await this.updatePermissionsTable(); await this.updatePermissionsTable();
})); }));
@@ -115,19 +121,40 @@ export abstract class PrincipalDialogBase<ObjectInfoType extends mssql.ObjectMan
this.securableSection = this.createGroup(localizedConstants.SecurablesText, items, true, true); this.securableSection = this.createGroup(localizedConstants.SecurablesText, items, true, true);
} }
private async onAddSecurableButtonClicked(): Promise<void> { private async onAddSecurableButtonClicked(button: azdata.ButtonComponent): Promise<void> {
const dialog = new FindObjectDialog(this.objectManagementService, { const selectedObjects: mssql.ObjectManagement.SearchResultItem[] = [];
objectTypes: this.viewInfo.supportedSecurableTypes, if (this.dialogOptions.isDatabaseLevelPrincipal) {
selectAllObjectTypes: false, const methodDialog = new ObjectSelectionMethodDialog({
multiSelect: true, objectTypes: this.viewInfo.supportedSecurableTypes,
contextId: this.contextId, schemas: (<mssql.ObjectManagement.DatabaseLevelPrincipalViewInfo<mssql.ObjectManagement.SecurityPrincipalObject>><unknown>this.viewInfo).schemas,
title: localizedConstants.SelectSecurablesDialogTitle, });
showSchemaColumn: this.showSchemaColumn await methodDialog.open();
}); const methodResult = await methodDialog.waitForClose();
await dialog.open(); if (methodResult) {
const result = await dialog.waitForClose(); switch (methodResult.method) {
if (result && result.selectedObjects.length > 0) { case ObjectSelectionMethod.AllObjectsOfTypes:
result.selectedObjects.forEach(obj => { 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)) { if (this.securablePermissions.find(securable => securable.type === obj.type && securable.name === obj.name && securable.schema === obj.schema)) {
return; return;
} }
@@ -150,6 +177,27 @@ export abstract class PrincipalDialogBase<ObjectInfoType extends mssql.ObjectMan
const data = this.getSecurableTableData(); const data = this.getSecurableTableData();
await this.setTableData(this.securableTable, data); await this.setTableData(this.securableTable, data);
} }
await button.focus();
}
private async searchForObjects(objectTypes: string[], schema: string = undefined): Promise<mssql.ObjectManagement.SearchResultItem[]> {
this.updateLoadingStatus(true);
const result = await this.objectManagementService.search(this.contextId, objectTypes, undefined, schema);
this.updateLoadingStatus(false);
return result;
}
private async openFindObjectsDialog(): Promise<FindObjectDialogResult> {
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<void> { private async onRemoveSecurableButtonClicked(): Promise<void> {
@@ -164,7 +212,7 @@ export abstract class PrincipalDialogBase<ObjectInfoType extends mssql.ObjectMan
private getSecurableTableData(): string[][] { private getSecurableTableData(): string[][] {
return this.securablePermissions.map(securable => { return this.securablePermissions.map(securable => {
const row = [securable.name, this.getSecurableTypeDisplayName(securable.type)]; const row = [securable.name, this.getSecurableTypeDisplayName(securable.type)];
if (this.showSchemaColumn) { if (this.dialogOptions.isDatabaseLevelPrincipal) {
row.splice(1, 0, securable.schema); row.splice(1, 0, securable.schema);
} }
return row; return row;
@@ -223,6 +271,6 @@ export abstract class PrincipalDialogBase<ObjectInfoType extends mssql.ObjectMan
} }
private get showEffectivePermissions(): boolean { private get showEffectivePermissions(): boolean {
return !this.options.isNewObject && this.supportEffectivePermissions; return !this.dialogOptions.isNewObject && this.dialogOptions.supportEffectivePermissions;
} }
} }

View File

@@ -28,7 +28,7 @@ export class ServerRoleDialog extends PrincipalDialogBase<ObjectManagement.Serve
constructor(objectManagementService: IObjectManagementService, options: ObjectManagementDialogOptions) { constructor(objectManagementService: IObjectManagementService, options: ObjectManagementDialogOptions) {
super(objectManagementService, options, false, false); super(objectManagementService, { ...options, isDatabaseLevelPrincipal: false, supportEffectivePermissions: false });
} }
protected override get helpUrl(): string { protected override get helpUrl(): string {

View File

@@ -32,7 +32,7 @@ export class UserDialog extends PrincipalDialogBase<ObjectManagement.User, Objec
private membershipTable: azdata.TableComponent; private membershipTable: azdata.TableComponent;
constructor(objectManagementService: IObjectManagementService, options: ObjectManagementDialogOptions) { constructor(objectManagementService: IObjectManagementService, options: ObjectManagementDialogOptions) {
super(objectManagementService, options, true); super(objectManagementService, { ...options, isDatabaseLevelPrincipal: true, supportEffectivePermissions: true });
} }
protected override get helpUrl(): string { protected override get helpUrl(): string {

View File

@@ -76,7 +76,7 @@ export abstract class DialogBase<DialogResult> {
public async open(): Promise<void> { public async open(): Promise<void> {
try { try {
this.onLoadingStatusChanged(true); this.updateLoadingStatus(true);
const initializeDialogPromise = new Promise<void>((async resolve => { const initializeDialogPromise = new Promise<void>((async resolve => {
this.dialogObject.registerContent(async view => { this.dialogObject.registerContent(async view => {
this._modelView = view; this._modelView = view;
@@ -97,7 +97,7 @@ export abstract class DialogBase<DialogResult> {
azdata.window.openDialog(this.dialogObject); azdata.window.openDialog(this.dialogObject);
await initializeDialogPromise; await initializeDialogPromise;
await this.initialize(); await this.initialize();
this.onLoadingStatusChanged(false); this.updateLoadingStatus(false);
} catch (err) { } catch (err) {
azdata.window.closeDialog(this.dialogObject); azdata.window.closeDialog(this.dialogObject);
throw err; throw err;
@@ -121,10 +121,10 @@ export abstract class DialogBase<DialogResult> {
return errors.length === 0; return errors.length === 0;
} }
protected createLabelInputContainer(label: string, input: azdata.InputBoxComponent | azdata.DropDownComponent): azdata.FlexContainer { protected createLabelInputContainer(label: string, component: azdata.Component, required: boolean = false): azdata.FlexContainer {
const labelComponent = this.modelView.modelBuilder.text().withProps({ width: DefaultLabelWidth, value: label, requiredIndicator: input.required }).component(); 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(); 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; return container;
} }
@@ -236,7 +236,7 @@ export abstract class DialogBase<DialogResult> {
return table; return table;
} }
protected addButtonsForTable(table: azdata.TableComponent, addButtonAriaLabel: string, removeButtonAriaLabel: string, addHandler: () => Promise<void>, removeHandler: () => Promise<void>): azdata.FlexContainer { protected addButtonsForTable(table: azdata.TableComponent, addButtonAriaLabel: string, removeButtonAriaLabel: string, addHandler: (button: azdata.ButtonComponent) => Promise<void>, removeHandler: (button: azdata.ButtonComponent) => Promise<void>): azdata.FlexContainer {
let addButton: azdata.ButtonComponent; let addButton: azdata.ButtonComponent;
let removeButton: azdata.ButtonComponent; let removeButton: azdata.ButtonComponent;
const updateButtons = () => { const updateButtons = () => {
@@ -244,11 +244,11 @@ export abstract class DialogBase<DialogResult> {
removeButton.enabled = table.selectedRows?.length === 1 && table.selectedRows[0] !== -1 && table.selectedRows[0] < table.data.length; removeButton.enabled = table.selectedRows?.length === 1 && table.selectedRows[0] !== -1 && table.selectedRows[0] < table.data.length;
} }
addButton = this.createButton(uiLoc.AddText, addButtonAriaLabel, async () => { addButton = this.createButton(uiLoc.AddText, addButtonAriaLabel, async () => {
await addHandler(); await addHandler(addButton);
updateButtons(); updateButtons();
}); });
removeButton = this.createButton(uiLoc.RemoveText, removeButtonAriaLabel, async () => { removeButton = this.createButton(uiLoc.RemoveText, removeButtonAriaLabel, async () => {
await removeHandler(); await removeHandler(removeButton);
if (table.selectedRows.length === 1 && table.selectedRows[0] >= table.data.length) { if (table.selectedRows.length === 1 && table.selectedRows[0] >= table.data.length) {
table.selectedRows = [table.data.length - 1]; table.selectedRows = [table.data.length - 1];
} }
@@ -308,6 +308,20 @@ export abstract class DialogBase<DialogResult> {
}).withItems(items, { flex: '0 0 auto' }).component(); }).withItems(items, { flex: '0 0 auto' }).component();
} }
protected createRadioButton(label: string, groupName: string, checked: boolean, handler: (checked: boolean) => Promise<void>): 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 { protected removeItem(container: azdata.DivContainer | azdata.FlexContainer, item: azdata.Component): void {
if (container.items.indexOf(item) !== -1) { if (container.items.indexOf(item) !== -1) {
container.removeItem(item); container.removeItem(item);
@@ -324,7 +338,7 @@ export abstract class DialogBase<DialogResult> {
} }
} }
protected onLoadingStatusChanged(isLoading: boolean): void { protected updateLoadingStatus(isLoading: boolean): void {
if (this._loadingComponent) { if (this._loadingComponent) {
this._loadingComponent.loading = isLoading; this._loadingComponent.loading = isLoading;
} }

View File

@@ -68,8 +68,8 @@ export abstract class ScriptableDialogBase<OptionsType extends ScriptableDialogO
await this.initializeUI(); await this.initializeUI();
} }
protected override onLoadingStatusChanged(isLoading: boolean): void { protected override updateLoadingStatus(isLoading: boolean): void {
super.onLoadingStatusChanged(isLoading); super.updateLoadingStatus(isLoading);
this._helpButton.enabled = !isLoading; this._helpButton.enabled = !isLoading;
this.dialogObject.okButton.enabled = this._scriptButton.enabled = isLoading ? false : this.isDirty; this.dialogObject.okButton.enabled = this._scriptButton.enabled = isLoading ? false : this.isDirty;
} }
@@ -80,7 +80,7 @@ export abstract class ScriptableDialogBase<OptionsType extends ScriptableDialogO
protected abstract generateScript(): Promise<string>; protected abstract generateScript(): Promise<string>;
private async onScriptButtonClick(): Promise<void> { private async onScriptButtonClick(): Promise<void> {
this.onLoadingStatusChanged(true); this.updateLoadingStatus(true);
try { try {
const isValid = await this.runValidation(); const isValid = await this.runValidation();
if (!isValid) { if (!isValid) {
@@ -104,7 +104,7 @@ export abstract class ScriptableDialogBase<OptionsType extends ScriptableDialogO
level: azdata.window.MessageLevel.Error level: azdata.window.MessageLevel.Error
}; };
} finally { } finally {
this.onLoadingStatusChanged(false); this.updateLoadingStatus(false);
} }
} }
} }

View File

@@ -22,9 +22,10 @@ import { ILogService } from 'vs/platform/log/common/log';
selector: 'modelview-groupContainer', selector: 'modelview-groupContainer',
template: ` template: `
<div *ngIf="hasHeader()" [class]="getHeaderClass()" (click)="changeState()" (keydown)="onKeyDown($event)" [tabindex]="isCollapsible()? 0 : -1" [attr.role]="isCollapsible() ? 'button' : null" [attr.aria-expanded]="isCollapsible() ? !collapsed : null"> <div *ngIf="hasHeader()" [class]="getHeaderClass()" (click)="changeState()" (keydown)="onKeyDown($event)" [tabindex]="isCollapsible()? 0 : -1" [attr.role]="isCollapsible() ? 'button' : null" [attr.aria-expanded]="isCollapsible() ? !collapsed : null">
{{_containerLayout.header}} {{header}}
</div> </div>
<!-- This extra div is needed so that the expanded state of the header is updated correctly. See https://github.com/microsoft/azuredatastudio/pull/16499 for more details --> <!-- This extra div is needed so that the expanded state of the header is updated correctly. See https://github.com/microsoft/azuredatastudio/pull/16499 for more details -->
<fieldset [attr.aria-label]="header" class="modelview-group-fieldset">
<div> <div>
<div #container *ngIf="items" class="modelview-group-container" [ngStyle]="CSSStyles"> <div #container *ngIf="items" class="modelview-group-container" [ngStyle]="CSSStyles">
<ng-container *ngFor="let item of items"> <ng-container *ngFor="let item of items">
@@ -37,6 +38,7 @@ import { ILogService } from 'vs/platform/log/common/log';
</ng-container> </ng-container>
</div> </div>
</div> </div>
</fieldset>
` `
}) })
export default class GroupContainer extends ContainerBase<GroupLayout, GroupContainerProperties> implements IComponent, OnDestroy, AfterViewInit { export default class GroupContainer extends ContainerBase<GroupLayout, GroupContainerProperties> implements IComponent, OnDestroy, AfterViewInit {
@@ -95,6 +97,10 @@ export default class GroupContainer extends ContainerBase<GroupLayout, GroupCont
return this.getPropertyOrDefault<boolean>((props) => props.collapsed, false); return this.getPropertyOrDefault<boolean>((props) => props.collapsed, false);
} }
public get header(): string {
return this._containerLayout?.header;
}
private hasHeader(): boolean { private hasHeader(): boolean {
return this._containerLayout && !!this._containerLayout.header; return this._containerLayout && !!this._containerLayout.header;
} }

View File

@@ -9,6 +9,13 @@
box-sizing: border-box; box-sizing: border-box;
} }
.modelview-group-fieldset {
border: none;
margin-inline: 0px;
padding-inline: 0px;
padding-block: 0px;
}
.modelview-group-row { .modelview-group-row {
display: table-row; display: table-row;
} }