create UI to manege privileges to a role

This commit is contained in:
Sylvain MATHIEU OBS 2023-09-18 14:22:41 +02:00
parent 9ca10d6d6a
commit 2c0cd88ca6
15 changed files with 346 additions and 25 deletions

View File

@ -309,6 +309,7 @@ import { ExportPortableProjectComponent } from './components/export-portable-pro
import { NodesMenuConfirmationDialogComponent } from './components/project-map/nodes-menu/nodes-menu-confirmation-dialog/nodes-menu-confirmation-dialog.component';
import { ConfirmationDeleteAllProjectsComponent } from './components/projects/confirmation-delete-all-projects/confirmation-delete-all-projects.component';
import { ProjectMapLockConfirmationDialogComponent } from './components/project-map/project-map-menu/project-map-lock-confirmation-dialog/project-map-lock-confirmation-dialog.component';
import { PrivilegeComponent } from './components/role-management/role-detail/privilege/privilege.component';
@NgModule({
declarations: [
@ -531,6 +532,7 @@ import { ProjectMapLockConfirmationDialogComponent } from './components/project-
NodesMenuConfirmationDialogComponent,
ConfirmationDeleteAllProjectsComponent,
ProjectMapLockConfirmationDialogComponent,
PrivilegeComponent,
],
imports: [
BrowserModule,

View File

@ -0,0 +1,4 @@
export interface IPrivilegesChange {
add: string[];
delete: string[];
}

View File

@ -0,0 +1,30 @@
<link rel="stylesheet" href="privilege.component.scss">
<div class="privileges">
<div class="header">
<div class="title">Privileges</div>
<div *ngIf="!disable">
<button mat-button *ngIf="!editMode">
<mat-icon (click)="editMode = true">edit</mat-icon>
</button>
<button *ngIf="editMode" mat-raised-button color="warn" (click)="editMode = false">Cancel</button>
<button *ngIf="editMode" mat-raised-button color="primary" (click)="close()">Update</button>
</div>
</div>
<div *ngFor="let privilege of privileges">
<div class="privilege">
<mat-checkbox
*ngIf="editMode"
[checked]="ownedPrivilegesList.includes(privilege.privilege_id)"
(change)="onPrivilegeChange($event.checked, privilege)"></mat-checkbox>
<div *ngIf="editMode; else actualPrivileges">
<div
[ngClass]="{add: changer.isAdded(privilege.privilege_id), delete: changer.isDeleted(privilege.privilege_id)}">
{{privilege.name}}</div>
</div>
<ng-template #actualPrivileges>
<div *ngIf="ownedPrivilegesList.includes(privilege.privilege_id)">{{privilege.name}}</div>
</ng-template>
</div>
</div>
</div>

View File

@ -0,0 +1,32 @@
.header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
padding-bottom: 20px;
padding-left: 10px;
}
.title {
justify-content: center;
}
.header > div > button {
margin: 10px;
}
.privilege {
display: flex;
flex-direction: row;
padding-left: 10px;
justify-content: space-between;
}
.add {
color: green;
}
.delete {
color: red;
text-decoration: line-through;
}

View File

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { PrivilegeComponent } from './privilege.component';
describe('PrivilegeComponent', () => {
let component: PrivilegeComponent;
let fixture: ComponentFixture<PrivilegeComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ PrivilegeComponent ]
})
.compileComponents();
fixture = TestBed.createComponent(PrivilegeComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,47 @@
import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core';
import {Privilege} from "@models/api/Privilege";
import {PrivilegeChange} from "@components/role-management/role-detail/privilege/privilegeChange";
import {IPrivilegesChange} from "@components/role-management/role-detail/privilege/IPrivilegesChange";
@Component({
selector: 'app-privilege',
templateUrl: './privilege.component.html',
styleUrls: ['./privilege.component.scss']
})
export class PrivilegeComponent implements OnInit {
@Input() disable = true;
@Input() privileges: Privilege[] = [];
@Input() ownedPrivilegesList: string[] = []
@Output() update: EventEmitter<IPrivilegesChange> = new EventEmitter<IPrivilegesChange>();
changer = new PrivilegeChange(this.ownedPrivilegesList);
private editModeState = false;
get editMode(): boolean {
return this.editModeState;
};
set editMode(state: boolean) {
if(state) {
this.changer = new PrivilegeChange(this.ownedPrivilegesList);
}
this.editModeState = state;
};
constructor() { }
ngOnInit(): void {
}
onPrivilegeChange(checked: boolean, privilege: Privilege) {
const id = privilege.privilege_id
if(checked) {
this.changer.add(id);
} else {
this.changer.delete(id);
}
}
close() {
this.update.emit(this.changer.get())
this.editMode = false;
}
}

View File

@ -0,0 +1,55 @@
import {PrivilegeChange} from "@components/role-management/role-detail/privilege/privilegeChange";
const one = "8a6cda1a-9842-4cbe-94da-a7eb97562c96";
const two = "97acaefe-b378-46d0-937e-e9aa22cf8a93";
const three = "8be17c2f-4d83-4492-a494-f354dbd8cc4f";
const four = "aaf2a160-5c7b-4775-98f7-a431b00c9e5a";
const five = "def0f758-b10b-4bc6-a4f4-6461cd5cdcfb"
const owned = [one, two, three]
describe('#PrivilegeChange', () => {
it('should create', () => {
const changer = new PrivilegeChange(owned);
});
it("Should add need privilege id", () => {
const changer = new PrivilegeChange(owned);
changer.add(four);
expect(changer.get().add).toEqual([four]);
});
it("Should not add already owned privilege id", () => {
const changer = new PrivilegeChange(owned);
changer.add(one);
expect(changer.get().add).toEqual([]);
});
it("Should delete owned privilege", () => {
const changer = new PrivilegeChange(owned);
changer.delete(one);
expect(changer.get().delete).toEqual([one]);
});
it("Should not delete not owned privilege id", () => {
const changer = new PrivilegeChange(owned);
changer.delete(four);
expect(changer.get().delete).toEqual([]);
})
it("Should reset already added id", () => {
const changer = new PrivilegeChange(owned);
changer.add(four);
expect(changer.get().add).toEqual([four]);
expect(changer.get().delete).toEqual([]);
changer.delete(four);
expect(changer.get().add).toEqual([]);
expect(changer.get().delete).toEqual([]);
});
it("Should reset already deleted id", () => {
const changer = new PrivilegeChange(owned);
changer.delete(one);
expect(changer.get().delete).toEqual([one]);
expect(changer.get().add).toEqual([]);
changer.add(one);
expect(changer.get().delete).toEqual([]);
expect(changer.get().add).toEqual([]);
});
});

View File

@ -0,0 +1,44 @@
import {IPrivilegesChange} from "@components/role-management/role-detail/privilege/IPrivilegesChange";
export class PrivilegeChange {
private change= {add: new Set<string>(), delete: new Set<string>()};
constructor(public ownedPrivileges: string[]) {
}
public add(id: string) {
if (this.change.delete.has(id)) {
this.change.delete.delete(id);
}
if (this.ownedPrivileges.includes(id)) {
return;
}
this.change.add.add(id);
}
public delete(id: string) {
if (this.change.add.has(id)) {
this.change.add.delete(id);
}
if (!this.ownedPrivileges.includes(id)) {
return;
}
this.change.delete.add(id);
}
isAdded(id: string): boolean {
return this.change.add.has(id);
}
isDeleted(id: string) {
return this.change.delete.has(id);
}
get(): IPrivilegesChange {
return {add: Array.from(this.change.add), delete: Array.from(this.change.delete)};
}
}

View File

@ -8,27 +8,27 @@
[routerLink]="['/controller', controller.id, 'management', 'roles']">
<mat-icon aria-label="Back to role management">keyboard_arrow_left</mat-icon>
</a>
<h1 class="col">Role {{role.name}} details</h1>
<h1 class="col">Role {{($role | async)?.name}} details</h1>
</div>
<div class="main">
<div class="details">
<div>
<mat-form-field>
<mat-label>Role name:</mat-label>
<input matInput type="text" [ngModel]="role.name">
<input matInput type="text" [ngModel]="($role | async)?.name">
</mat-form-field>
</div>
<div>
<mat-form-field>
<mat-label>Description:</mat-label>
<input matInput type="text" [ngModel]="role.description">
<input matInput type="text" [ngModel]="($role | async)?.description">
</mat-form-field>
</div>
<div>Creation date: {{role.created_at}}</div>
<div>Last update Date: {{role.updated_at}}</div>
<div>UUID: {{role.role_id}}</div>
<div>Creation date: {{($role | async)?.created_at}}</div>
<div>Last update Date: {{($role | async )?.updated_at}}</div>
<div>UUID: {{($role | async)?.role_id}}</div>
<div>
<mat-checkbox [checked]="role.is_builtin" disabled>Is build in</mat-checkbox>
<mat-checkbox [checked]="($role | async)?.is_builtin" disabled>Is build in</mat-checkbox>
</div>
<div mat-dialog-actions class="button-div">
<button mat-button (click)="onUpdate()" tabindex="2" mat-raised-button color="primary"
@ -38,7 +38,11 @@
</div>
</div>
<mat-divider [vertical]="true"></mat-divider>
<app-privilege
[disable]="($role | async)?.is_builtin"
[ownedPrivilegesList]="$ownedPrivilegesId | async"
[privileges]="privileges | async"
(update)="onPrivilegesUpdate($event)"></app-privilege>
</div>
</div>
</div>

View File

@ -10,11 +10,22 @@
justify-content: center;
}
.permissions {
.privileges {
width: 35vw;
display: flex;
flex-direction: column;
justify-content: stretch;
justify-content: center;
}
.clickable {
cursor: pointer;
}
.privilege {
display: flex;
flex-direction: row;
padding-left: 10px;
justify-content: space-between;
}
.permission {
@ -33,7 +44,7 @@
flex-direction: row;
justify-content: space-between;
padding-bottom: 20px;
padding-left: 10px;
}
.header > div {
font-size: 2em;

View File

@ -14,11 +14,15 @@ import {Component, OnInit} from '@angular/core';
import {RoleService} from "@services/role.service";
import {ActivatedRoute} from "@angular/router";
import {Controller} from "@models/controller";
import {ControllerService} from "@services/controller.service";
import {Role} from "@models/api/role";
import {UntypedFormControl, UntypedFormGroup} from "@angular/forms";
import {ToasterService} from "@services/toaster.service";
import {HttpErrorResponse} from "@angular/common/http";
import {Privilege} from "@models/api/Privilege";
import {PrivilegeService} from "@services/privilege.service";
import {Observable, ReplaySubject} from "rxjs";
import {IPrivilegesChange} from "@components/role-management/role-detail/privilege/IPrivilegesChange";
import {map} from "rxjs/operators";
@Component({
selector: 'app-role-detail',
@ -27,13 +31,20 @@ import {HttpErrorResponse} from "@angular/common/http";
})
export class RoleDetailComponent implements OnInit {
controller: Controller;
role: Role;
$role: ReplaySubject<Role> = new ReplaySubject<Role>(1);
editRoleForm: UntypedFormGroup;
$ownedPrivilegesId: Observable<string[]> = this.$role.pipe(map((role: Role) => {
return role.privileges.map((p: Privilege) => p.privilege_id);
}));
privileges: Observable<Privilege[]>;
private roleId: string;
constructor(private roleService: RoleService,
private controllerService: ControllerService,
private toastService: ToasterService,
private route: ActivatedRoute) {
private route: ActivatedRoute,
private privilegeService: PrivilegeService,
) {
this.editRoleForm = new UntypedFormGroup({
rolename: new UntypedFormControl(),
@ -43,19 +54,40 @@ export class RoleDetailComponent implements OnInit {
ngOnInit(): void {
this.route.data.subscribe((d: { controller: Controller; role: Role }) => {
this.controller = d.controller;
this.role = d.role;
this.controller = d.controller
this.roleId = d.role.role_id;
this.privileges = this.privilegeService.get(this.controller);
this.roleService.getById(this.controller, this.roleId).subscribe((role: Role) => this.$role.next(role))
});
}
onUpdate() {
this.roleService.update(this.controller, this.role)
.subscribe(() => {
this.toastService.success(`role: ${this.role.name} was updated`);
},
(error: HttpErrorResponse) => {
this.toastService.error(`${error.message}
this.$role.subscribe((role) => {
this.roleService.update(this.controller, role)
.subscribe(() => {
this.toastService.success(`role: ${role.name} was updated`);
this.roleService.getById(this.controller, this.roleId).subscribe((role: Role) => this.$role.next(role))
},
(error: HttpErrorResponse) => {
this.toastService.error(`${error.message}
${error.error.message}`);
});
});
})
}
onPrivilegesUpdate(privileges: IPrivilegesChange) {
const tasks = [];
for (const privilege of privileges.add) {
tasks.push(this.roleService.setPrivileges(this.controller, this.roleId, privilege));
}
for (const privilege of privileges.delete) {
tasks.push(this.roleService.removePrivileges(this.controller, this.roleId, privilege));
}
Observable
.forkJoin(tasks)
.subscribe(() => {
this.roleService.getById(this.controller, this.roleId).subscribe((role: Role) => this.$role.next(role))
});
}
}

View File

@ -0,0 +1,7 @@
export interface Privilege {
name: string;
description: string;
created_at: string;
updated_at: string;
privilege_id: string;
}

View File

@ -1,3 +1,5 @@
import {Privilege} from "@models/api/Privilege";
export interface Role {
name: string;
description: string;
@ -5,4 +7,5 @@ export interface Role {
updated_at: string;
role_id: string;
is_builtin: boolean;
privileges: Privilege[];
}

View File

@ -0,0 +1,17 @@
import { Injectable } from '@angular/core';
import {HttpController} from "@services/http-controller.service";
import {Controller} from "@models/controller";
import {Privilege} from "@models/api/Privilege";
@Injectable({
providedIn: 'root'
})
export class PrivilegeService {
constructor(private httpController: HttpController) { }
get(controller: Controller) {
return this.httpController.get<Privilege[]>(controller, "/access/privileges")
}
}

View File

@ -15,6 +15,7 @@ import {HttpController} from "./http-controller.service";
import {Controller} from "../models/controller";
import {Group} from "../models/groups/group";
import {Role} from "../models/api/role";
import {Privilege} from "@models/api/Privilege";
@Injectable({
providedIn: 'root'
@ -42,4 +43,13 @@ export class RoleService {
update(controller: Controller, role: Role) {
return this.httpController.put(controller, `/access/roles/${role.role_id}`, {name: role.name, description: role.description});
}
setPrivileges(controller: Controller, roleId: string, privilegeId: string) {
return this.httpController.put(controller, `/access/roles/${roleId}/privileges/${privilegeId}`, undefined);
}
removePrivileges(controller: Controller, roleId: string, privilegeId: string) {
return this.httpController.delete(controller, `/access/roles/${roleId}/privileges/${privilegeId}`);
}
}