Browse Source

Add resource permission management component

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
Fahri Gedik 2 months ago
parent
commit
667554fd8e
  1. 63
      npm/ng-packs/packages/permission-management/proxy/src/lib/proxy/models.ts
  2. 88
      npm/ng-packs/packages/permission-management/proxy/src/lib/proxy/permissions.service.ts
  3. 1
      npm/ng-packs/packages/permission-management/src/lib/components/index.ts
  4. 174
      npm/ng-packs/packages/permission-management/src/lib/components/resource-permission-management.component.html
  5. 325
      npm/ng-packs/packages/permission-management/src/lib/components/resource-permission-management.component.ts

63
npm/ng-packs/packages/permission-management/proxy/src/lib/proxy/models.ts

@ -4,6 +4,27 @@ export interface GetPermissionListResultDto {
groups: PermissionGroupDto[];
}
export interface GetResourcePermissionDefinitionListResultDto {
permissions?: ResourcePermissionDefinitionDto[];
}
export interface GetResourcePermissionListResultDto {
permissions?: ResourcePermissionGrantInfoDto[];
}
export interface GetResourcePermissionWithProviderListResultDto {
permissions?: ResourcePermissionWithProdiverGrantInfoDto[];
}
export interface GetResourceProviderListResultDto {
providers?: ResourceProviderDto[];
}
export interface GrantedResourcePermissionDto {
name?: string;
displayName?: string;
}
export interface PermissionGrantInfoDto {
name?: string;
displayName?: string;
@ -17,6 +38,8 @@ export interface PermissionGroupDto {
name?: string;
displayName?: string;
permissions: PermissionGrantInfoDto[];
displayNameKey?: string;
displayNameResource?: string;
}
export interface ProviderInfoDto {
@ -24,6 +47,40 @@ export interface ProviderInfoDto {
providerKey?: string;
}
export interface ResourcePermissionDefinitionDto {
name?: string;
displayName?: string;
}
export interface ResourcePermissionGrantInfoDto {
providerName?: string;
providerKey?: string;
providerDisplayName?: string;
providerNameDisplayName?: string;
permissions?: GrantedResourcePermissionDto[];
}
export interface ResourcePermissionWithProdiverGrantInfoDto {
name?: string;
displayName?: string;
providers?: string[];
isGranted?: boolean;
}
export interface ResourceProviderDto {
name?: string;
displayName?: string;
}
export interface SearchProviderKeyInfo {
providerKey?: string;
providerDisplayName?: string;
}
export interface SearchProviderKeyListResultDto {
keys?: SearchProviderKeyInfo[];
}
export interface UpdatePermissionDto {
name?: string;
isGranted: boolean;
@ -32,3 +89,9 @@ export interface UpdatePermissionDto {
export interface UpdatePermissionsDto {
permissions: UpdatePermissionDto[];
}
export interface UpdateResourcePermissionsDto {
providerName?: string;
providerKey?: string;
permissions?: string[];
}

88
npm/ng-packs/packages/permission-management/proxy/src/lib/proxy/permissions.service.ts

@ -1,5 +1,5 @@
import type { GetPermissionListResultDto, UpdatePermissionsDto } from './models';
import { RestService } from '@abp/ng.core';
import type { GetPermissionListResultDto, GetResourcePermissionDefinitionListResultDto, GetResourcePermissionListResultDto, GetResourcePermissionWithProviderListResultDto, GetResourceProviderListResultDto, SearchProviderKeyListResultDto, UpdatePermissionsDto, UpdateResourcePermissionsDto } from './models';
import { RestService, Rest } from '@abp/ng.core';
import { Injectable, inject } from '@angular/core';
@Injectable({
@ -7,23 +7,97 @@ import { Injectable, inject } from '@angular/core';
})
export class PermissionsService {
private restService = inject(RestService);
apiName = 'AbpPermissionManagement';
get = (providerName: string, providerKey: string) =>
deleteResource = (resourceName: string, resourceKey: string, providerName: string, providerKey: string, config?: Partial<Rest.Config>) =>
this.restService.request<any, void>({
method: 'DELETE',
url: '/api/permission-management/permissions/resource',
params: { resourceName, resourceKey, providerName, providerKey },
},
{ apiName: this.apiName, ...config });
get = (providerName: string, providerKey: string, config?: Partial<Rest.Config>) =>
this.restService.request<any, GetPermissionListResultDto>({
method: 'GET',
url: '/api/permission-management/permissions',
params: { providerName, providerKey },
},
{ apiName: this.apiName });
{ apiName: this.apiName, ...config });
getByGroup = (groupName: string, providerName: string, providerKey: string, config?: Partial<Rest.Config>) =>
this.restService.request<any, GetPermissionListResultDto>({
method: 'GET',
url: '/api/permission-management/permissions/by-group',
params: { groupName, providerName, providerKey },
},
{ apiName: this.apiName, ...config });
getResource = (resourceName: string, resourceKey: string, config?: Partial<Rest.Config>) =>
this.restService.request<any, GetResourcePermissionListResultDto>({
method: 'GET',
url: '/api/permission-management/permissions/resource',
params: { resourceName, resourceKey },
},
{ apiName: this.apiName, ...config });
getResourceByProvider = (resourceName: string, resourceKey: string, providerName: string, providerKey: string, config?: Partial<Rest.Config>) =>
this.restService.request<any, GetResourcePermissionWithProviderListResultDto>({
method: 'GET',
url: '/api/permission-management/permissions/resource/by-provider',
params: { resourceName, resourceKey, providerName, providerKey },
},
{ apiName: this.apiName, ...config });
update = (providerName: string, providerKey: string, input: UpdatePermissionsDto) =>
getResourceDefinitions = (resourceName: string, config?: Partial<Rest.Config>) =>
this.restService.request<any, GetResourcePermissionDefinitionListResultDto>({
method: 'GET',
url: '/api/permission-management/permissions/resource-definitions',
params: { resourceName },
},
{ apiName: this.apiName, ...config });
getResourceProviderKeyLookupServices = (resourceName: string, config?: Partial<Rest.Config>) =>
this.restService.request<any, GetResourceProviderListResultDto>({
method: 'GET',
url: '/api/permission-management/permissions/resource-provider-key-lookup-services',
params: { resourceName },
},
{ apiName: this.apiName, ...config });
searchResourceProviderKey = (resourceName: string, serviceName: string, filter: string, page: number, config?: Partial<Rest.Config>) =>
this.restService.request<any, SearchProviderKeyListResultDto>({
method: 'GET',
url: '/api/permission-management/permissions/search-resource-provider-keys',
params: { resourceName, serviceName, filter, page },
},
{ apiName: this.apiName, ...config });
update = (providerName: string, providerKey: string, input: UpdatePermissionsDto, config?: Partial<Rest.Config>) =>
this.restService.request<any, void>({
method: 'PUT',
url: '/api/permission-management/permissions',
params: { providerName, providerKey },
body: input,
},
{ apiName: this.apiName });
{ apiName: this.apiName, ...config });
updateResource = (resourceName: string, resourceKey: string, input: UpdateResourcePermissionsDto, config?: Partial<Rest.Config>) =>
this.restService.request<any, void>({
method: 'PUT',
url: '/api/permission-management/permissions/resource',
params: { resourceName, resourceKey },
body: input,
},
{ apiName: this.apiName, ...config });
}

1
npm/ng-packs/packages/permission-management/src/lib/components/index.ts

@ -1 +1,2 @@
export * from './permission-management.component';
export * from './resource-permission-management.component';

174
npm/ng-packs/packages/permission-management/src/lib/components/resource-permission-management.component.html

@ -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>

325
npm/ng-packs/packages/permission-management/src/lib/components/resource-permission-management.component.ts

@ -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…
Cancel
Save