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 './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