Merge branch 'RED-3804' - Configure entity permissions

This commit is contained in:
Adina Țeudan 2022-04-06 15:39:08 +03:00
commit 6ed84d9ca0
21 changed files with 338 additions and 3 deletions

View File

@ -125,6 +125,9 @@ const components = [AppComponent, AuthErrorComponent, NotificationsComponent, Sp
PDF: {
enabled: false,
},
STATS: {
enabled: false,
},
},
} as ILoggerConfig,
},

View File

@ -0,0 +1,16 @@
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivate } from '@angular/router';
import { firstValueFrom } from 'rxjs';
import { EntityPermissionsService } from '../services/entity-permissions/entity-permissions.service';
@Injectable({ providedIn: 'root' })
export class PermissionsGuard implements CanActivate {
constructor(private readonly _entityPermissionsService: EntityPermissionsService) {}
async canActivate(route: ActivatedRouteSnapshot): Promise<boolean> {
const targetObject = route.data.permissionsObject;
await firstValueFrom(this._entityPermissionsService.loadConfigFor(targetObject));
await firstValueFrom(this._entityPermissionsService.loadFor(targetObject));
return true;
}
}

View File

@ -24,6 +24,7 @@ import { DossierStatesListingScreenComponent } from './screens/dossier-states-li
import { DossiersGuard } from '@guards/dossiers.guard';
import { ACTIVE_DOSSIERS_SERVICE } from '../../tokens';
import { BaseEntityScreenComponent } from './base-entity-screen/base-entity-screen.component';
import { PermissionsGuard } from '../../guards/permissions-guard';
const routes: Routes = [
{ path: '', redirectTo: 'dossier-templates', pathMatch: 'full' },
@ -159,6 +160,16 @@ const routes: Routes = [
requiredRoles: ['RED_USER_ADMIN'],
},
},
{
path: 'dossier-permissions',
component: BaseAdminScreenComponent,
canActivate: [CompositeRouteGuard],
data: {
routeGuards: [AuthGuard, RedRoleGuard, PermissionsGuard],
permissionsObject: 'Dossier',
},
loadChildren: () => import('./screens/permissions/permissions.module').then(m => m.PermissionsModule),
},
{
path: 'license-info',
component: LicenseInformationScreenComponent,

View File

@ -44,6 +44,7 @@ export class AdminSideNavComponent implements OnInit {
},
{ screen: 'audit', label: _('admin-side-nav.audit'), hideIf: !this.currentUser.isAdmin },
{ screen: 'users', label: _('admin-side-nav.user-management'), hideIf: !this.currentUser.isUserAdmin },
{ screen: 'dossier-permissions', label: _('dossier-permissions'), hideIf: !this.currentUser.isAdmin },
{
screen: 'general-config',
label: _('admin-side-nav.configurations'),

View File

@ -0,0 +1,20 @@
import { Injectable } from '@angular/core';
import { TableColumnConfig } from '@iqser/common-ui';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { PermissionsMapping } from '@red/domain';
import { PermissionsConfigurationMapService } from '@services/entity-permissions/permissions-configuration-map.service';
import { permissionsTranslations } from '../../translations/permissions-translations';
@Injectable()
export class ConfigService {
constructor(private readonly _permissionsConfigurationMapService: PermissionsConfigurationMapService) {}
tableConfig(targetObject: string): TableColumnConfig<PermissionsMapping>[] {
const columns = this._permissionsConfigurationMapService.getMappedPermissions(targetObject).map(p => ({
label: permissionsTranslations.mapped[p],
class: 'flex-center',
}));
return [{ label: _('permissions-screen.table-col-names.permission') }, ...columns];
}
}

View File

@ -0,0 +1,20 @@
<iqser-page-header
(closeAction)="routerHistoryService.navigateToLastDossiersScreen()"
[pageLabel]="'permissions-screen.label' | translate: { targetObject: this.targetObject }"
[showCloseButton]="currentUser.isUser"
></iqser-page-header>
<iqser-table [itemSize]="80" [tableColumnConfigs]="tableColumnConfigs"></iqser-table>
<ng-template #tableItemTemplate let-entity="entity">
<div *ngIf="cast(entity) as config">
<div class="cell">{{ translations[targetObject][config.searchKey] | translate }}</div>
<div *ngFor="let permission of mappedPermissions" class="center cell">
<mat-slide-toggle
(toggleChange)="togglePermission(config.searchKey, permission)"
[checked]="config.getValue(permission)"
color="primary"
></mat-slide-toggle>
</div>
</div>
</ng-template>

View File

@ -0,0 +1,4 @@
:host {
flex-grow: 1;
overflow: hidden;
}

View File

@ -0,0 +1,64 @@
import { ChangeDetectionStrategy, Component, forwardRef, Injector } from '@angular/core';
import { DefaultListingServices, ListingComponent, LoadingService, SortingOrders, TableColumnConfig } from '@iqser/common-ui';
import { PermissionsMapping } from '@red/domain';
import { ConfigService } from '../config.service';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { EntityPermissionsService } from '@services/entity-permissions/entity-permissions.service';
import { ActivatedRoute } from '@angular/router';
import { PermissionsMapService } from '@services/entity-permissions/permissions-map.service';
import { PermissionsConfigurationMapService } from '@services/entity-permissions/permissions-configuration-map.service';
import { firstValueFrom } from 'rxjs';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { tap } from 'rxjs/operators';
import { permissionsTranslations } from '../../../translations/permissions-translations';
import { UserService } from '@services/user.service';
import { RouterHistoryService } from '@services/router-history.service';
@UntilDestroy()
@Component({
templateUrl: './permissions-screen.component.html',
styleUrls: ['./permissions-screen.component.scss'],
providers: [...DefaultListingServices, { provide: ListingComponent, useExisting: forwardRef(() => PermissionsScreenComponent) }],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PermissionsScreenComponent extends ListingComponent<PermissionsMapping> {
readonly currentUser = this._userService.currentUser;
readonly translations = permissionsTranslations;
readonly tableColumnConfigs: TableColumnConfig<PermissionsMapping>[];
readonly tableHeaderLabel = _('permissions-screen.table-header.title');
readonly targetObject: string;
readonly mappedPermissions: string[];
constructor(
protected readonly _injector: Injector,
private readonly _configService: ConfigService,
private readonly _userService: UserService,
private readonly _loadingService: LoadingService,
private readonly _entityPermissionsService: EntityPermissionsService,
private readonly _permissionsMapService: PermissionsMapService,
private readonly _permissionsConfigurationMapService: PermissionsConfigurationMapService,
private readonly _route: ActivatedRoute,
readonly routerHistoryService: RouterHistoryService,
) {
super(_injector);
this.targetObject = _route.snapshot.data.permissionsObject;
this.tableColumnConfigs = this._configService.tableConfig(this.targetObject);
this.mappedPermissions = this._permissionsConfigurationMapService.getMappedPermissions(this.targetObject);
this.entitiesService.setEntities(this._permissionsMapService.get(this.targetObject));
this.sortingService.setSortingOption({
column: 'sort',
order: SortingOrders.asc,
});
this._permissionsMapService
.get$(this.targetObject)
.pipe(tap(permissions => this.entitiesService.setEntities(permissions)))
.pipe(untilDestroyed(this))
.subscribe();
}
async togglePermission(targetPermission: string, changedPermission: string): Promise<void> {
this._loadingService.start();
await firstValueFrom(this._entityPermissionsService.togglePermission(this.targetObject, targetPermission, changedPermission));
this._loadingService.stop();
}
}

View File

@ -0,0 +1,16 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule } from '@angular/router';
import { SharedModule } from '@shared/shared.module';
import { PermissionsScreenComponent } from './permissions-screen/permissions-screen.component';
import { ConfigService } from './config.service';
import { CommonUiModule } from '@iqser/common-ui';
const routes = [{ path: '', component: PermissionsScreenComponent }];
@NgModule({
declarations: [PermissionsScreenComponent],
imports: [RouterModule.forChild(routes), CommonModule, SharedModule, CommonUiModule],
providers: [ConfigService],
})
export class PermissionsModule {}

View File

@ -0,0 +1,14 @@
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
export const permissionsTranslations: Record<string, Record<string, string>> = {
mapped: {
OWNER: _('permissions-screen.mapped.owner'),
APPROVE: _('permissions-screen.mapped.approve'),
REVIEW: _('permissions-screen.mapped.review'),
EVERYONE_ELSE: _('permissions-screen.mapped.everyone-else'),
},
Dossier: {
VIEW_OBJECT: _('permissions-screen.dossier.view'),
ACCESS_OBJECT: _('permissions-screen.dossier.access'),
},
} as const;

View File

@ -57,18 +57,18 @@ export class AnnotationDetailsComponent implements OnChanges {
[
{
icon: 'red:dictionary',
description: 'annotation-engines.dictionary',
description: _('annotation-engines.dictionary'),
show: AnnotationDetailsComponent._isBasedOn(annotation, Engines.DICTIONARY),
translateParams: { isHint: this.annotation.hint },
},
{
icon: 'red:ai',
description: 'annotation-engines.ner',
description: _('annotation-engines.ner'),
show: AnnotationDetailsComponent._isBasedOn(annotation, Engines.NER),
},
{
icon: 'red:rule',
description: 'annotation-engines.rule',
description: _('annotation-engines.rule'),
show: AnnotationDetailsComponent._isBasedOn(annotation, Engines.RULE),
translateParams: { rule: this.annotation.legalBasisValue || '' },
},

View File

@ -0,0 +1,49 @@
import { Injectable, Injector } from '@angular/core';
import { GenericService, mapEach } from '@iqser/common-ui';
import { IPermissionsMapping, PermissionsMapping } from '@red/domain';
import { Observable } from 'rxjs';
import { switchMap, tap } from 'rxjs/operators';
import { PermissionsConfigurationMapService } from './permissions-configuration-map.service';
import { PermissionsMapService } from './permissions-map.service';
@Injectable({
providedIn: 'root',
})
export class EntityPermissionsService extends GenericService<IPermissionsMapping> {
constructor(
protected readonly _injector: Injector,
private readonly _permissionsConfigurationMapService: PermissionsConfigurationMapService,
private readonly _permissionsMapService: PermissionsMapService,
) {
super(_injector, 'permissions');
}
loadConfigFor(targetObject: string): Observable<PermissionsMapping[]> {
return this._http.get<IPermissionsMapping[]>(`/${this._defaultModelPath}/${targetObject}/mapping`).pipe(
mapEach(mapping => new PermissionsMapping(mapping, targetObject)),
tap(mappings => this._permissionsConfigurationMapService.set(targetObject, mappings)),
);
}
loadFor(targetObject: string): Observable<PermissionsMapping[]> {
return this._http.get<IPermissionsMapping[]>(`/${this._defaultModelPath}/${targetObject}`).pipe(
mapEach(mapping => new PermissionsMapping(mapping, targetObject)),
tap(mappings => this._permissionsMapService.set(targetObject, mappings)),
);
}
togglePermission(targetObject: string, targetPermission: string, changedPermission: string): Observable<PermissionsMapping[]> {
const config = this._permissionsConfigurationMapService.get(targetObject);
const targetPermissionConfig = config.find(p => p.searchKey === targetPermission);
const permissions = this._permissionsMapService.get(targetObject);
const currentTargetPermissionValues = this._permissionsMapService.get(targetObject, targetPermission);
const index = currentTargetPermissionValues.mappedPermissions.findIndex(p => p.name === changedPermission);
if (index !== -1) {
currentTargetPermissionValues.mappedPermissions.splice(index, 1);
} else {
const permission = targetPermissionConfig.mappedPermissions.find(p => p.name === changedPermission);
currentTargetPermissionValues.mappedPermissions.push(permission);
}
return this._post(permissions, `${this._defaultModelPath}/${targetObject}`).pipe(switchMap(() => this.loadFor(targetObject)));
}
}

View File

@ -0,0 +1,20 @@
import { Injectable } from '@angular/core';
import { IPermissionsMapping, PermissionsMapping } from '@red/domain';
import { EntitiesMapService } from '@iqser/common-ui';
@Injectable({ providedIn: 'root' })
export class PermissionsConfigurationMapService extends EntitiesMapService<PermissionsMapping, IPermissionsMapping> {
constructor() {
super('name');
}
getMappedPermissions(targetObject: string): string[] {
return Array.from(
new Set(
this.get(targetObject)
.flatMap(p => p.mappedPermissions)
.map(p => p.name),
),
);
}
}

View File

@ -0,0 +1,10 @@
import { Injectable } from '@angular/core';
import { IPermissionsMapping, PermissionsMapping } from '@red/domain';
import { EntitiesMapService } from '@iqser/common-ui';
@Injectable({ providedIn: 'root' })
export class PermissionsMapService extends EntitiesMapService<PermissionsMapping, IPermissionsMapping> {
constructor() {
super('name');
}
}

View File

@ -864,6 +864,7 @@
"under-review": "In Review",
"upload-files": "Sie können Dateien überall per Drag and Drop platzieren..."
},
"dossier-permissions": "",
"dossier-states-listing": {
"action": {
"delete": "",
@ -1626,6 +1627,25 @@
},
"toggle-tooltips": "{active, select, true{Disable} false{Enable} other{}} Kurzinfos für Anmerkungen"
},
"permissions-screen": {
"dossier": {
"access": "",
"view": ""
},
"label": "",
"mapped": {
"approve": "",
"everyone-else": "",
"owner": "",
"review": ""
},
"table-col-names": {
"permission": ""
},
"table-header": {
"title": ""
}
},
"processing-status": {
"ocr": "",
"pending": "",

View File

@ -864,6 +864,7 @@
"under-review": "Under Review",
"upload-files": "Drag & drop files anywhere..."
},
"dossier-permissions": "Dossier Permissions",
"dossier-states-listing": {
"action": {
"delete": "Delete State",
@ -1626,6 +1627,25 @@
},
"toggle-tooltips": "{active, select, true{Disable} false{Enable} other{}} annotation tooltips"
},
"permissions-screen": {
"dossier": {
"access": "Access Dossier",
"view": "View Dossier"
},
"label": "{targetObject, select, Dossier{Dossier} other{}} Permissions",
"mapped": {
"approve": "Approvers",
"everyone-else": "Everyone else",
"owner": "Owner",
"review": "Reviewers"
},
"table-col-names": {
"permission": "Permission"
},
"table-header": {
"title": "{length} {length, plural, one{Permission} other{Permissions}}"
}
},
"processing-status": {
"ocr": "OCR",
"pending": "Pending",

View File

@ -23,3 +23,4 @@ export * from './lib/dossier-stats';
export * from './lib/dossier-state';
export * from './lib/trash';
export * from './lib/text-highlight';
export * from './lib/permissions';

View File

@ -0,0 +1,3 @@
export * from './permissions-mapping';
export * from './permissions';
export * from './permissions-mapping.model';

View File

@ -0,0 +1,32 @@
import { Entity } from '@iqser/common-ui';
import { IPermissionsMapping } from './permissions-mapping';
import { IPermission } from './permissions';
export class PermissionsMapping extends Entity<IPermissionsMapping> implements IPermissionsMapping {
readonly routerLink = undefined;
readonly mappedPermissions: IPermission[];
readonly targetPermission: IPermission;
readonly sort: number;
readonly #currentValuesMap = new Map<string, boolean>();
constructor(permissionsMapping: IPermissionsMapping, readonly targetObject: string) {
super(permissionsMapping);
this.mappedPermissions = permissionsMapping.mappedPermissions;
this.targetPermission = permissionsMapping.targetPermission;
this.sort = this.targetPermission.sort;
this.mappedPermissions.forEach(permission => this.#currentValuesMap.set(permission.name, true));
}
get id(): string {
return this.targetPermission.name;
}
get searchKey(): string {
return this.targetPermission.name;
}
getValue(permissionName: string): boolean {
return this.#currentValuesMap.has(permissionName);
}
}

View File

@ -0,0 +1,6 @@
import { IPermission } from './permissions';
export interface IPermissionsMapping {
mappedPermissions: IPermission[];
targetPermission: IPermission;
}

View File

@ -0,0 +1,5 @@
export interface IPermission {
mask: number;
name: string;
sort: number;
}