mirror of https://github.com/abpframework/abp.git
Browse Source
Introduces ResourcePermissionManagementComponent with UI and logic for managing resource-based permissions. Updates models and service to support resource permission APIs, and exports the new component in the index.test-branch-24299
5 changed files with 644 additions and 7 deletions
@ -1 +1,2 @@ |
|||
export * from './permission-management.component'; |
|||
export * from './resource-permission-management.component'; |
|||
|
|||
@ -0,0 +1,174 @@ |
|||
<abp-modal [(visible)]="visible" [busy]="modalBusy()" [options]="{ size: 'xl', scrollable: false }"> |
|||
<ng-template #abpHeader> |
|||
<h4> |
|||
{{ 'AbpPermissionManagement::ResourcePermissions' | abpLocalization }} |
|||
@if (resourceDisplayName) { |
|||
- {{ resourceDisplayName }} |
|||
} |
|||
</h4> |
|||
</ng-template> |
|||
|
|||
<ng-template #abpBody> |
|||
@if (!hasResourcePermission() || !hasProviderKeyLookupService()) { |
|||
<div class="alert alert-warning" role="alert"> |
|||
@if (!hasResourcePermission()) { |
|||
{{ 'AbpPermissionManagement::NoResourcePermissionFound' | abpLocalization }} |
|||
} @else { |
|||
{{ 'AbpPermissionManagement::NoResourceProviderKeyLookupServiceFound' | abpLocalization }} |
|||
} |
|||
</div> |
|||
} @else { |
|||
<!-- LIST VIEW --> |
|||
@if (viewMode() === 'list') { |
|||
<div class="d-grid gap-2 mb-2 d-md-flex justify-content-md-end"> |
|||
<button class="btn btn-sm btn-primary" type="button" (click)="goToAddMode()"> |
|||
{{ 'AbpPermissionManagement::AddResourcePermission' | abpLocalization }} |
|||
</button> |
|||
</div> |
|||
|
|||
@if (resourcePermissions().length === 0) { |
|||
<div class="alert alert-info"> |
|||
{{ 'AbpPermissionManagement::NoPermissionsAssigned' | abpLocalization }} |
|||
</div> |
|||
} @else { |
|||
<table class="table table-striped"> |
|||
<thead> |
|||
<tr> |
|||
<th>{{ 'AbpPermissionManagement::Provider' | abpLocalization }}</th> |
|||
<th>{{ 'AbpPermissionManagement::ProviderKey' | abpLocalization }}</th> |
|||
<th>{{ 'AbpPermissionManagement::Permissions' | abpLocalization }}</th> |
|||
<th>{{ 'AbpPermissionManagement::Actions' | abpLocalization }}</th> |
|||
</tr> |
|||
</thead> |
|||
<tbody> |
|||
@for (grant of resourcePermissions(); track grant.providerKey) { |
|||
<tr> |
|||
<td>{{ grant.providerNameDisplayName }}</td> |
|||
<td>{{ grant.providerDisplayName || grant.providerKey }}</td> |
|||
<td> |
|||
@for (perm of grant.permissions; track perm.name) { |
|||
<span class="badge bg-primary me-1">{{ perm.displayName }}</span> |
|||
} |
|||
</td> |
|||
<td> |
|||
<button class="btn btn-sm btn-outline-primary me-1" (click)="goToEditMode(grant)"> |
|||
<i class="fa fa-edit"></i> |
|||
</button> |
|||
<button class="btn btn-sm btn-outline-danger" (click)="deletePermission(grant)"> |
|||
<i class="fa fa-trash"></i> |
|||
</button> |
|||
</td> |
|||
</tr> |
|||
} |
|||
</tbody> |
|||
</table> |
|||
} |
|||
} |
|||
|
|||
<!-- ADD VIEW --> |
|||
@if (viewMode() === 'add') { |
|||
<div class="mb-3"> |
|||
<div class="mb-2"> |
|||
@for (provider of providers(); track provider.name; let i = $index) { |
|||
<div class="form-check form-check-inline"> |
|||
<input class="form-check-input" type="radio" [id]="'provider-' + i" [value]="provider.name" |
|||
[checked]="selectedProviderName() === provider.name" |
|||
(change)="onProviderChange(provider.name || '')" /> |
|||
<label class="form-check-label" [for]="'provider-' + i"> |
|||
{{ provider.displayName }} |
|||
</label> |
|||
</div> |
|||
} |
|||
</div> |
|||
|
|||
<div class="position-relative mb-3"> |
|||
<input type="text" class="form-control" |
|||
[placeholder]="'AbpPermissionManagement::SearchProviderKey' | abpLocalization" |
|||
[ngModel]="searchFilter()" (ngModelChange)="onSearchInput($event)" /> |
|||
@if (searchResults().length > 0) { |
|||
<div class="list-group position-absolute w-100 shadow" |
|||
style="z-index: 1000; max-height: 200px; overflow-y: auto;"> |
|||
@for (result of searchResults(); track result.providerKey) { |
|||
<button type="button" class="list-group-item list-group-item-action" |
|||
(click)="selectProviderKey(result)"> |
|||
{{ result.providerDisplayName || result.providerKey }} |
|||
</button> |
|||
} |
|||
</div> |
|||
} |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="mb-3"> |
|||
<h5>{{ 'AbpPermissionManagement::ResourcePermissionPermissions' | abpLocalization }}</h5> |
|||
<div class="form-check form-switch mb-2"> |
|||
<input class="form-check-input" type="checkbox" id="grantAllAdd" [checked]="allPermissionsSelected()" |
|||
(change)="toggleAllPermissions(!allPermissionsSelected())" /> |
|||
<label class="form-check-label" for="grantAllAdd"> |
|||
{{ 'AbpPermissionManagement::GrantAllResourcePermissions' | abpLocalization }} |
|||
</label> |
|||
</div> |
|||
@for (perm of permissionDefinitions(); track perm.name) { |
|||
<div class="form-check"> |
|||
<input class="form-check-input" type="checkbox" [id]="'perm-add-' + perm.name" |
|||
[checked]="isPermissionSelected(perm.name || '')" (change)="togglePermission(perm.name || '')" /> |
|||
<label class="form-check-label" [for]="'perm-add-' + perm.name"> |
|||
{{ perm.displayName }} |
|||
</label> |
|||
</div> |
|||
} |
|||
</div> |
|||
} |
|||
|
|||
<!-- EDIT VIEW --> |
|||
@if (viewMode() === 'edit') { |
|||
<div class="mb-3"> |
|||
<h5>{{ 'AbpPermissionManagement::ResourcePermissionPermissions' | abpLocalization }}</h5> |
|||
<p class="text-muted"> |
|||
{{ editProviderName() }}: {{ editProviderKey() }} |
|||
</p> |
|||
<div class="form-check form-switch mb-2"> |
|||
<input class="form-check-input" type="checkbox" id="grantAllEdit" [checked]="allPermissionsSelected()" |
|||
(change)="toggleAllPermissions(!allPermissionsSelected())" /> |
|||
<label class="form-check-label" for="grantAllEdit"> |
|||
{{ 'AbpPermissionManagement::GrantAllResourcePermissions' | abpLocalization }} |
|||
</label> |
|||
</div> |
|||
@for (perm of permissionsWithProvider(); track perm.name) { |
|||
<div class="form-check"> |
|||
<input class="form-check-input" type="checkbox" [id]="'perm-edit-' + perm.name" |
|||
[checked]="isPermissionSelected(perm.name || '')" (change)="togglePermission(perm.name || '')" /> |
|||
<label class="form-check-label" [for]="'perm-edit-' + perm.name"> |
|||
{{ perm.displayName }} |
|||
@for (provider of perm.providers; track provider) { |
|||
<span class="badge bg-secondary ms-1">{{ provider }}</span> |
|||
} |
|||
</label> |
|||
</div> |
|||
} |
|||
</div> |
|||
} |
|||
} |
|||
</ng-template> |
|||
|
|||
<ng-template #abpFooter> |
|||
@if (viewMode() === 'list') { |
|||
<button type="button" class="btn btn-outline-primary" abpClose> |
|||
{{ 'AbpUi::Close' | abpLocalization }} |
|||
</button> |
|||
} @else { |
|||
<button type="button" class="btn btn-outline-secondary" (click)="goToListMode()"> |
|||
{{ 'AbpUi::Cancel' | abpLocalization }} |
|||
</button> |
|||
@if (viewMode() === 'add') { |
|||
<abp-button iconClass="fa fa-check" (click)="saveAddPermission()"> |
|||
{{ 'AbpUi::Save' | abpLocalization }} |
|||
</abp-button> |
|||
} @else { |
|||
<abp-button iconClass="fa fa-check" (click)="saveEditPermission()"> |
|||
{{ 'AbpUi::Save' | abpLocalization }} |
|||
</abp-button> |
|||
} |
|||
} |
|||
</ng-template> |
|||
</abp-modal> |
|||
@ -0,0 +1,325 @@ |
|||
import { LocalizationPipe } from '@abp/ng.core'; |
|||
import { |
|||
ButtonComponent, |
|||
ModalCloseDirective, |
|||
ModalComponent, |
|||
ToasterService, |
|||
} from '@abp/ng.theme.shared'; |
|||
import { |
|||
GetResourcePermissionListResultDto, |
|||
GetResourceProviderListResultDto, |
|||
GetResourcePermissionDefinitionListResultDto, |
|||
GetResourcePermissionWithProviderListResultDto, |
|||
PermissionsService, |
|||
ResourcePermissionGrantInfoDto, |
|||
ResourceProviderDto, |
|||
SearchProviderKeyInfo, |
|||
ResourcePermissionDefinitionDto, |
|||
ResourcePermissionWithProdiverGrantInfoDto, |
|||
} from '@abp/ng.permission-management/proxy'; |
|||
import { |
|||
ChangeDetectionStrategy, |
|||
Component, |
|||
EventEmitter, |
|||
inject, |
|||
Input, |
|||
Output, |
|||
signal, |
|||
computed, |
|||
} from '@angular/core'; |
|||
import { FormsModule } from '@angular/forms'; |
|||
import { finalize, switchMap, of, debounceTime, Subject, distinctUntilChanged } from 'rxjs'; |
|||
|
|||
type ViewMode = 'list' | 'add' | 'edit'; |
|||
|
|||
@Component({ |
|||
selector: 'abp-resource-permission-management', |
|||
templateUrl: './resource-permission-management.component.html', |
|||
exportAs: 'abpResourcePermissionManagement', |
|||
changeDetection: ChangeDetectionStrategy.OnPush, |
|||
imports: [ |
|||
FormsModule, |
|||
ModalComponent, |
|||
LocalizationPipe, |
|||
ButtonComponent, |
|||
ModalCloseDirective, |
|||
], |
|||
}) |
|||
export class ResourcePermissionManagementComponent { |
|||
protected readonly service = inject(PermissionsService); |
|||
protected readonly toasterService = inject(ToasterService); |
|||
|
|||
@Input() resourceName!: string; |
|||
@Input() resourceKey!: string; |
|||
@Input() resourceDisplayName?: string; |
|||
|
|||
protected _visible = false; |
|||
|
|||
@Input() |
|||
get visible(): boolean { |
|||
return this._visible; |
|||
} |
|||
|
|||
set visible(value: boolean) { |
|||
if (value === this._visible) return; |
|||
|
|||
if (value) { |
|||
this.openModal(); |
|||
} else { |
|||
this.resetState(); |
|||
} |
|||
this._visible = value; |
|||
this.visibleChange.emit(value); |
|||
} |
|||
|
|||
@Output() readonly visibleChange = new EventEmitter<boolean>(); |
|||
|
|||
// State signals
|
|||
viewMode = signal<ViewMode>('list'); |
|||
modalBusy = signal(false); |
|||
hasResourcePermission = signal(false); |
|||
hasProviderKeyLookupService = signal(false); |
|||
|
|||
// Data
|
|||
resourcePermissions = signal<ResourcePermissionGrantInfoDto[]>([]); |
|||
providers = signal<ResourceProviderDto[]>([]); |
|||
permissionDefinitions = signal<ResourcePermissionDefinitionDto[]>([]); |
|||
searchResults = signal<SearchProviderKeyInfo[]>([]); |
|||
permissionsWithProvider = signal<ResourcePermissionWithProdiverGrantInfoDto[]>([]); |
|||
|
|||
// Form state for add/edit
|
|||
selectedProviderName = signal<string>(''); |
|||
selectedProviderKey = signal<string>(''); |
|||
searchFilter = signal(''); |
|||
selectedPermissions = signal<string[]>([]); |
|||
|
|||
// Edit mode state
|
|||
editProviderName = signal<string>(''); |
|||
editProviderKey = signal<string>(''); |
|||
|
|||
// Search subject for debounce
|
|||
private searchSubject = new Subject<string>(); |
|||
|
|||
constructor() { |
|||
this.searchSubject.pipe( |
|||
debounceTime(300), |
|||
distinctUntilChanged() |
|||
).subscribe(filter => { |
|||
this.performSearch(filter); |
|||
}); |
|||
} |
|||
|
|||
openModal() { |
|||
this.modalBusy.set(true); |
|||
|
|||
// Load resource permissions and providers
|
|||
this.service.getResource(this.resourceName, this.resourceKey).pipe( |
|||
switchMap(permRes => { |
|||
this.resourcePermissions.set(permRes.permissions || []); |
|||
return this.service.getResourceProviderKeyLookupServices(this.resourceName); |
|||
}), |
|||
switchMap(providerRes => { |
|||
this.providers.set(providerRes.providers || []); |
|||
this.hasProviderKeyLookupService.set((providerRes.providers?.length || 0) > 0); |
|||
if (providerRes.providers?.length) { |
|||
this.selectedProviderName.set(providerRes.providers[0].name || ''); |
|||
} |
|||
return this.service.getResourceDefinitions(this.resourceName); |
|||
}), |
|||
finalize(() => this.modalBusy.set(false)) |
|||
).subscribe({ |
|||
next: defRes => { |
|||
this.permissionDefinitions.set(defRes.permissions || []); |
|||
this.hasResourcePermission.set((defRes.permissions?.length || 0) > 0); |
|||
}, |
|||
error: () => { |
|||
this.toasterService.error('AbpPermissionManagement::ErrorLoadingPermissions'); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
resetState() { |
|||
this.viewMode.set('list'); |
|||
this.resourcePermissions.set([]); |
|||
this.selectedProviderName.set(''); |
|||
this.selectedProviderKey.set(''); |
|||
this.searchFilter.set(''); |
|||
this.selectedPermissions.set([]); |
|||
this.searchResults.set([]); |
|||
} |
|||
|
|||
// View mode navigation
|
|||
goToAddMode() { |
|||
this.viewMode.set('add'); |
|||
this.selectedPermissions.set([]); |
|||
this.selectedProviderKey.set(''); |
|||
this.searchResults.set([]); |
|||
} |
|||
|
|||
goToEditMode(grant: ResourcePermissionGrantInfoDto) { |
|||
this.editProviderName.set(grant.providerName || ''); |
|||
this.editProviderKey.set(grant.providerKey || ''); |
|||
this.modalBusy.set(true); |
|||
|
|||
this.service.getResourceByProvider( |
|||
this.resourceName, |
|||
this.resourceKey, |
|||
grant.providerName || '', |
|||
grant.providerKey || '' |
|||
).pipe( |
|||
finalize(() => this.modalBusy.set(false)) |
|||
).subscribe({ |
|||
next: res => { |
|||
this.permissionsWithProvider.set(res.permissions || []); |
|||
this.selectedPermissions.set( |
|||
(res.permissions || []).filter(p => p.isGranted).map(p => p.name || '') |
|||
); |
|||
this.viewMode.set('edit'); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
goToListMode() { |
|||
this.viewMode.set('list'); |
|||
this.selectedPermissions.set([]); |
|||
} |
|||
|
|||
// Provider selection
|
|||
onProviderChange(providerName: string) { |
|||
this.selectedProviderName.set(providerName); |
|||
this.selectedProviderKey.set(''); |
|||
this.searchResults.set([]); |
|||
this.searchFilter.set(''); |
|||
} |
|||
|
|||
// Search
|
|||
onSearchInput(filter: string) { |
|||
this.searchFilter.set(filter); |
|||
this.searchSubject.next(filter); |
|||
} |
|||
|
|||
private performSearch(filter: string) { |
|||
if (!filter || !this.selectedProviderName()) return; |
|||
|
|||
this.service.searchResourceProviderKey( |
|||
this.resourceName, |
|||
this.selectedProviderName(), |
|||
filter, |
|||
1 |
|||
).subscribe(res => { |
|||
this.searchResults.set(res.keys || []); |
|||
}); |
|||
} |
|||
|
|||
selectProviderKey(key: SearchProviderKeyInfo) { |
|||
this.selectedProviderKey.set(key.providerKey || ''); |
|||
this.searchFilter.set(key.providerDisplayName || key.providerKey || ''); |
|||
this.searchResults.set([]); |
|||
} |
|||
|
|||
// Permission toggle
|
|||
togglePermission(permissionName: string) { |
|||
const current = this.selectedPermissions(); |
|||
if (current.includes(permissionName)) { |
|||
this.selectedPermissions.set(current.filter(p => p !== permissionName)); |
|||
} else { |
|||
this.selectedPermissions.set([...current, permissionName]); |
|||
} |
|||
} |
|||
|
|||
toggleAllPermissions(selectAll: boolean) { |
|||
if (this.viewMode() === 'add') { |
|||
this.selectedPermissions.set( |
|||
selectAll |
|||
? this.permissionDefinitions().map(p => p.name || '') |
|||
: [] |
|||
); |
|||
} else { |
|||
this.selectedPermissions.set( |
|||
selectAll |
|||
? this.permissionsWithProvider().map(p => p.name || '') |
|||
: [] |
|||
); |
|||
} |
|||
} |
|||
|
|||
isPermissionSelected(permissionName: string): boolean { |
|||
return this.selectedPermissions().includes(permissionName); |
|||
} |
|||
|
|||
allPermissionsSelected = computed(() => { |
|||
const definitions = this.viewMode() === 'add' |
|||
? this.permissionDefinitions() |
|||
: this.permissionsWithProvider(); |
|||
return definitions.length > 0 && |
|||
definitions.every(p => this.selectedPermissions().includes(p.name || '')); |
|||
}); |
|||
|
|||
// Save operations
|
|||
saveAddPermission() { |
|||
if (!this.selectedProviderKey() || this.selectedPermissions().length === 0) { |
|||
this.toasterService.warn('AbpPermissionManagement::PleaseSelectProviderAndPermissions'); |
|||
return; |
|||
} |
|||
|
|||
this.modalBusy.set(true); |
|||
this.service.updateResource( |
|||
this.resourceName, |
|||
this.resourceKey, |
|||
{ |
|||
providerName: this.selectedProviderName(), |
|||
providerKey: this.selectedProviderKey(), |
|||
permissions: this.selectedPermissions() |
|||
} |
|||
).pipe( |
|||
switchMap(() => this.service.getResource(this.resourceName, this.resourceKey)), |
|||
finalize(() => this.modalBusy.set(false)) |
|||
).subscribe({ |
|||
next: res => { |
|||
this.resourcePermissions.set(res.permissions || []); |
|||
this.toasterService.success('AbpUi::SavedSuccessfully'); |
|||
this.goToListMode(); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
saveEditPermission() { |
|||
this.modalBusy.set(true); |
|||
this.service.updateResource( |
|||
this.resourceName, |
|||
this.resourceKey, |
|||
{ |
|||
providerName: this.editProviderName(), |
|||
providerKey: this.editProviderKey(), |
|||
permissions: this.selectedPermissions() |
|||
} |
|||
).pipe( |
|||
switchMap(() => this.service.getResource(this.resourceName, this.resourceKey)), |
|||
finalize(() => this.modalBusy.set(false)) |
|||
).subscribe({ |
|||
next: res => { |
|||
this.resourcePermissions.set(res.permissions || []); |
|||
this.toasterService.success('AbpUi::SavedSuccessfully'); |
|||
this.goToListMode(); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
deletePermission(grant: ResourcePermissionGrantInfoDto) { |
|||
this.modalBusy.set(true); |
|||
this.service.deleteResource( |
|||
this.resourceName, |
|||
this.resourceKey, |
|||
grant.providerName || '', |
|||
grant.providerKey || '' |
|||
).pipe( |
|||
switchMap(() => this.service.getResource(this.resourceName, this.resourceKey)), |
|||
finalize(() => this.modalBusy.set(false)) |
|||
).subscribe({ |
|||
next: res => { |
|||
this.resourcePermissions.set(res.permissions || []); |
|||
this.toasterService.success('AbpUi::SuccessfullyDeleted'); |
|||
} |
|||
}); |
|||
} |
|||
} |
|||
Loading…
Reference in new issue