You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@archiva.apache.org by ma...@apache.org on 2020/12/22 23:07:10 UTC

[archiva] branch master updated (8866128 -> 3012e2f)

This is an automated email from the ASF dual-hosted git repository.

martin_s pushed a change to branch master
in repository https://gitbox.apache.org/repos/asf/archiva.git.


    from 8866128  Additional angular code
     new 130e4a3  Adding role information
     new b988a30  Adding loading spinner
     new b247e09  Adding user role assignment
     new 3012e2f  Updating role management

The 4 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .../src/app/model/{operation.ts => role-update.ts} |  10 +-
 .../src/main/archiva-web/src/app/model/role.ts     |  11 +
 .../app/modules/security/role-routing.module.ts    |   1 +
 .../manage-roles-edit.component.html               | 254 ++++++++++++++++++-
 .../manage-roles-edit.component.ts                 | 270 ++++++++++++++++++++-
 .../manage-roles-list.component.html               |  71 +++---
 .../manage-roles-list.component.ts                 |  23 +-
 .../roles/manage-roles/manage-roles.component.ts   |   1 -
 .../security/users/manage-users-base.component.ts  |   4 +-
 .../manage-users-delete.component.ts               |   4 +-
 .../manage-users-edit.component.html               |   2 +-
 .../manage-users-list.component.html               | 112 +++++----
 .../manage-users-list.component.ts                 |  14 +-
 .../manage-users-roles.component.html              |   2 +-
 .../users/manage-users/manage-users.component.ts   |   1 -
 .../app/modules/shared/about/about.component.html  |   5 +-
 .../app/modules/shared/about/about.component.scss  |   5 +-
 .../modules/shared/about/about.component.spec.ts   |   3 +-
 .../app/modules/shared/about/about.component.ts    |   3 +-
 .../modules/shared/contact/contact.component.html  |   5 +-
 .../modules/shared/contact/contact.component.scss  |   5 +-
 .../shared/contact/contact.component.spec.ts       |   3 +-
 .../modules/shared/contact/contact.component.ts    |   3 +-
 .../edit-base.component.ts}                        |  95 +++-----
 .../app/modules/shared/home/home.component.html    |   5 +-
 .../app/modules/shared/home/home.component.scss    |   5 +-
 .../app/modules/shared/home/home.component.spec.ts |   3 +-
 .../src/app/modules/shared/home/home.component.ts  |   3 +-
 .../app/modules/shared/login/login.component.html  |   5 +-
 .../app/modules/shared/login/login.component.scss  |   5 +-
 .../modules/shared/login/login.component.spec.ts   |   3 +-
 .../app/modules/shared/login/login.component.ts    |   3 +-
 .../shared/model/loading-value.spec.ts}            |   6 +-
 .../src/app/modules/shared/model/loading-value.ts  |  62 +++++
 .../shared/model/page-query.spec.ts}               |   6 +-
 .../shared/model/page-query.ts}                    |   5 +-
 .../shared/not-found/not-found.component.html      |   5 +-
 .../shared/not-found/not-found.component.scss      |   5 +-
 .../shared/not-found/not-found.component.spec.ts   |   3 +-
 .../shared/not-found/not-found.component.ts        |   3 +-
 .../paginated-entities.component.html              |  10 +-
 .../paginated-entities.component.ts                |  92 +++++--
 .../src/app/modules/shared/shared.module.ts        |  23 +-
 .../shared/sidemenu/sidemenu.component.html        |   5 +-
 .../shared/sidemenu/sidemenu.component.scss        |   5 +-
 .../shared/sidemenu/sidemenu.component.spec.ts     |   3 +-
 .../modules/shared/sidemenu/sidemenu.component.ts  |   3 +-
 .../shared/sorted-table-component.ts}              |  13 +-
 .../shared/strip-loading.pipe.spec.ts}             |   9 +-
 .../strip-loading.pipe.ts}                         |  23 +-
 .../shared/with-loading.pipe.spec.ts}              |   9 +-
 .../with-loading.pipe.ts}                          |  32 ++-
 .../src/app/services/archiva-request.service.ts    |   4 +
 .../archiva-web/src/app/services/role.service.ts   |  79 +++++-
 .../src/main/archiva-web/src/assets/i18n/en.json   |  31 ++-
 55 files changed, 1080 insertions(+), 295 deletions(-)
 copy archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/model/{operation.ts => role-update.ts} (83%)
 copy archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/{security/users/manage-users-base.component.ts => shared/edit-base.component.ts} (60%)
 copy archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/{model/operation.spec.ts => modules/shared/model/loading-value.spec.ts} (87%)
 create mode 100644 archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/model/loading-value.ts
 copy archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/{model/role.spec.ts => modules/shared/model/page-query.spec.ts} (88%)
 copy archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/{model/application.ts => modules/shared/model/page-query.ts} (89%)
 copy archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/{model/error-result.ts => modules/shared/sorted-table-component.ts} (73%)
 copy archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/{model/access-token.spec.ts => modules/shared/strip-loading.pipe.spec.ts} (80%)
 copy archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/{security/roles/manage-roles-edit/manage-roles-edit.component.ts => shared/strip-loading.pipe.ts} (60%)
 copy archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/{model/access-token.spec.ts => modules/shared/with-loading.pipe.spec.ts} (80%)
 copy archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/{repo/upload/upload.component.ts => shared/with-loading.pipe.ts} (51%)


[archiva] 04/04: Updating role management

Posted by ma...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

martin_s pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/archiva.git

commit 3012e2f76ff672dccefa306585391b21bfe24f8a
Author: Martin Stockhammer <ma...@apache.org>
AuthorDate: Tue Dec 22 20:27:51 2020 +0100

    Updating role management
---
 .../manage-roles-edit.component.html               | 14 ++++-
 .../manage-roles-edit.component.ts                 | 69 ++++++++++++++++++++--
 .../manage-roles-list.component.ts                 |  2 +-
 .../security/users/manage-users-base.component.ts  |  4 +-
 .../manage-users-delete.component.ts               |  4 +-
 .../manage-users-list.component.ts                 |  2 +-
 .../manage-users-roles.component.html              |  2 +-
 .../users/manage-users/manage-users.component.ts   |  1 -
 .../paginated-entities.component.ts                | 37 ++++++++----
 .../archiva-web/src/app/services/role.service.ts   | 30 ++++++++++
 10 files changed, 141 insertions(+), 24 deletions(-)

diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/roles/manage-roles-edit/manage-roles-edit.component.html b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/roles/manage-roles-edit/manage-roles-edit.component.html
index bf6dd1e..d6fba7e 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/roles/manage-roles-edit/manage-roles-edit.component.html
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/roles/manage-roles-edit/manage-roles-edit.component.html
@@ -163,6 +163,7 @@
             <h4>{{'roles.edit.usersParents'|translate}}</h4>
             <app-paginated-entities [service]="roleUserParentService" pageSize="5" [(sortField)]="userParentSortField"
                                     [(sortOrder)]="userParentSortOrder" [displayControlsIfSinglePage]="false"
+                                    [id]="'userParentSection'"
                                     #userParentSection>
 
                 <ng-container *ngIf="userParentSection.items$ |async as itemLoader">
@@ -198,8 +199,10 @@
             <hr/>
             <h4>{{'roles.edit.usersInstance'|translate}}</h4>
             <app-paginated-entities [service]="roleUserService" pageSize="5" [(sortField)]="userSortField"
+                                    [id]="'userSection'"
                                     [(sortOrder)]="userSortOrder"
                                     [displayIfEmpty]="false" [displayKeyIfEmpty]="'roles.edit.noUsersAssigned'"
+                                    [displayControlsIfSinglePage]="false"
                                     #userSection>
 
                 <ng-container *ngIf="userSection.items$ |async as itemLoader">
@@ -219,6 +222,7 @@
                                            contentText="users.attributes.user_id"></app-th-sorted>
                             <app-th-sorted [fieldArray]="['full_name']"
                                            contentText="users.attributes.full_name"></app-th-sorted>
+                            <th>{{'headers.action'|translate}}</th>
                         </tr>
                         </thead>
                         <tbody>
@@ -227,6 +231,8 @@
                                       ngbTooltip="{{user.id}}">{{user.user_id}}</span>
                             </td>
                             <td>{{user.full_name}}</td>
+                            <td><a href="javascript: void(0)" (click)="unassignUser(user.user_id)"><span
+                                    class="fas fa-user-minus"></span></a></td>
                         </tr>
                         </tbody>
                     </table>
@@ -235,7 +241,7 @@
             <hr/>
             <form class="mt-2">
                 <ng-template #userResultTemplate let-r="result" let-t="term">
-                    <ngb-highlight [result]="r.user_id + '-' + r.full_name" [term]="t"></ngb-highlight>
+                    <ngb-highlight [result]="r.user_id + ' - ' + r.full_name" [term]="t"></ngb-highlight>
                 </ng-template>
                 <div class="form-group">
                     <label for="typeahead-http">{{'roles.edit.assignUserSearch'|translate}}</label>
@@ -254,6 +260,12 @@
             </form>
 
         </ng-template>
+        <div *ngIf="success">
+            Success
+        </div>
+        <div *ngIf="error">
+            <div>Error {{errorResult.error_messages}}</div>
+        </div>
     </ngb-panel>
 
 </ngb-accordion>
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/roles/manage-roles-edit/manage-roles-edit.component.ts b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/roles/manage-roles-edit/manage-roles-edit.component.ts
index 71655fe..321557b 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/roles/manage-roles-edit/manage-roles-edit.component.ts
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/roles/manage-roles-edit/manage-roles-edit.component.ts
@@ -16,7 +16,16 @@
  * under the License.
  */
 
-import {AfterContentInit, Component, EventEmitter, OnInit, Output} from '@angular/core';
+import {
+    AfterContentInit,
+    ChangeDetectorRef,
+    Component,
+    EventEmitter,
+    OnInit,
+    Output,
+    TemplateRef,
+    ViewChild
+} from '@angular/core';
 import {ActivatedRoute} from "@angular/router";
 import {FormBuilder, Validators} from "@angular/forms";
 import {RoleService} from "@app/services/role.service";
@@ -31,6 +40,8 @@ import {User} from '@app/model/user';
 import {PagedResult} from "@app/model/paged-result";
 import {UserService} from "@app/services/user.service";
 import {UserInfo} from '@app/model/user-info';
+import {HttpResponse} from "@angular/common/http";
+import {PaginatedEntitiesComponent} from "@app/modules/shared/paginated-entities/paginated-entities.component";
 
 @Component({
     selector: 'app-manage-roles-edit',
@@ -55,11 +66,14 @@ export class ManageRolesEditComponent extends EditBaseComponent<Role> implements
 
     public userSearchModel:any;
 
+    @ViewChild('userSection') roleUserComponent: PaginatedEntitiesComponent<UserInfo>;
+    @ViewChild('userParentSection') roleUserParentComponent: PaginatedEntitiesComponent<UserInfo>;
 
     @Output()
     roleIdEvent: EventEmitter<string> = new EventEmitter<string>(true);
 
-    constructor(private route: ActivatedRoute, public roleService: RoleService, private userService: UserService, public fb: FormBuilder) {
+    constructor(private route: ActivatedRoute, public roleService: RoleService, private userService: UserService,
+                public fb: FormBuilder, private changeDetect : ChangeDetectorRef) {
         super(fb);
         super.init(fb.group({
             id: [''],
@@ -100,6 +114,12 @@ export class ManageRolesEditComponent extends EditBaseComponent<Role> implements
             this.roleUserParentService = function (searchTerm: string, offset: number, limit: number, orderBy: string[], order: string): Observable<PagedResult<User>> {
                 return fRoleService.queryAssignedParentUsers(roleId, searchTerm, offset, limit, orderBy, order, true);
             };
+            if (this.roleUserComponent) {
+                this.roleUserComponent.changeService(this.roleUserService);
+            }
+            if (this.roleUserParentComponent) {
+                this.roleUserParentComponent.changeService(this.roleUserParentService);
+            }
         }, error => {
             this.editRole = new Role();
         });
@@ -166,7 +186,7 @@ export class ManageRolesEditComponent extends EditBaseComponent<Role> implements
         let role = new RoleUpdate();
         role.id=this.userForm.get('id').value;
         role.description = this.userForm.get('description').value;
-        console.log("Submitting changes " + role);
+        // console.log("Submitting changes " + role);
         this.roleService.updateRole(role).pipe(
             catchError((err: ErrorResult) => {
                 this.error = true;
@@ -185,6 +205,7 @@ export class ManageRolesEditComponent extends EditBaseComponent<Role> implements
     }
 
     ngAfterContentInit(): void {
+        // console.log("AfterContentInit")
         if (this.originRole) {
             this.editRole = this.originRole;
         }
@@ -196,7 +217,7 @@ export class ManageRolesEditComponent extends EditBaseComponent<Role> implements
             distinctUntilChanged(),
             tap(() => this.userSearching = true),
             switchMap(term =>
-                this.userService.query(term, 0, 10).pipe(
+                this.roleService.queryUnAssignedUsers(this.editRole.id, term, 0, 10).pipe(
                     tap(() => this.userSearchFailed = false),
                     map(pagedResult=>
                     pagedResult.data),
@@ -221,7 +242,45 @@ export class ManageRolesEditComponent extends EditBaseComponent<Role> implements
                 userId = this.userSearchModel.user_id;
             }
         }
-        console.log("Assigning user " + userId)
+        if (this.editRole.id!=null && userId!=null && userId.length>0) {
+            this.roleService.assignRole(this.editRole.id, userId).pipe(
+                catchError((err: ErrorResult) => {
+                    this.error = true;
+                    this.success = false;
+                    this.errorResult = err;
+                    return [];
+                })
+            ).subscribe((response : HttpResponse<Role>)  => {
+                this.error = false;
+                this.success = true;
+                this.errorResult = null;
+                this.result = response.body;
+                this.roleUserComponent.changePage(1);
+                this.userSearchModel=''
+            });
+        }
+    }
+
+    unassignUser(user_id:string) {
+        // console.log("Unassigning " + this.editRole.id + " - " + user_id);
+        if (this.editRole.id!=null && user_id!=null && user_id.length>0) {
+            this.roleService.unAssignRole(this.editRole.id, user_id).pipe(
+                catchError((err: ErrorResult) => {
+                    this.error = true;
+                    this.success = false;
+                    this.errorResult = err;
+                    return [];
+                })
+            ).subscribe((response : HttpResponse<Role>)  => {
+                    // console.log("Deleted ");
+                    this.error = false;
+                    this.success = true;
+                    this.errorResult = null;
+                    this.result = response.body;
+                    this.roleUserComponent.changePage(1);
+                }
+            );
+        }
     }
 
 }
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/roles/manage-roles-list/manage-roles-list.component.ts b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/roles/manage-roles-list/manage-roles-list.component.ts
index 1344b11..982f9f2 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/roles/manage-roles-list/manage-roles-list.component.ts
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/roles/manage-roles-list/manage-roles-list.component.ts
@@ -38,7 +38,7 @@ export class ManageRolesListComponent extends SortedTableComponent<Role> impleme
 
   constructor(translator: TranslateService, roleService : RoleService, private ngbModal:NgbModal) {
     super(translator, function (searchTerm: string, offset: number, limit: number, orderBy: string[], order: string): Observable<PagedResult<Role>> {
-      console.log("Retrieving data " + searchTerm + "," + offset + "," + limit + "," + orderBy + "," + order);
+      // console.log("Retrieving data " + searchTerm + "," + offset + "," + limit + "," + orderBy + "," + order);
       return roleService.query(searchTerm, offset, limit, orderBy, order);
     });
   }
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/users/manage-users-base.component.ts b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/users/manage-users-base.component.ts
index 42a27e8..012d704 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/users/manage-users-base.component.ts
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/users/manage-users-base.component.ts
@@ -68,7 +68,7 @@ export class ManageUsersBaseComponent {
         for (let prop of properties) {
             user[prop] = this.userForm.get(prop).value;
         }
-        console.log("User " + user);
+        // console.log("User " + user);
         return user;
     }
 
@@ -79,7 +79,7 @@ export class ManageUsersBaseComponent {
             propMap[prop] = propValue;
         }
         this.userForm.patchValue(propMap);
-        console.log("User " + user);
+        // console.log("User " + user);
     }
 
 
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/users/manage-users-delete/manage-users-delete.component.ts b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/users/manage-users-delete/manage-users-delete.component.ts
index d646e20..005fda8 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/users/manage-users-delete/manage-users-delete.component.ts
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/users/manage-users-delete/manage-users-delete.component.ts
@@ -48,7 +48,7 @@ export class ManageUsersDeleteComponent implements OnInit, AfterViewInit {
   private runModal() {
     if (this.user_id!=null && this.user_id!='') {
       let modalInstance = this.modal.open(this.askModal).result.then((result) => {
-        console.log("Result: " + result);
+        // console.log("Result: " + result);
         let userId = this.user_id;
         if (result=='YES' && userId!=null && userId!='') {
           let deleted = this.userService.deleteUser(userId).subscribe();
@@ -59,7 +59,7 @@ export class ManageUsersDeleteComponent implements OnInit, AfterViewInit {
           this.router.navigate(['/security','users','list']);
         }
       }, (reason) => {
-        console.log("Reason: " + reason);
+        // console.log("Reason: " + reason);
       });
     }
   }
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/users/manage-users-list/manage-users-list.component.ts b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/users/manage-users-list/manage-users-list.component.ts
index b22aeea..abc81a0 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/users/manage-users-list/manage-users-list.component.ts
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/users/manage-users-list/manage-users-list.component.ts
@@ -39,7 +39,7 @@ export class ManageUsersListComponent implements OnInit {
 
   constructor(private translator: TranslateService, private userService : UserService) {
     this.service = function (searchTerm: string, offset: number, limit: number, orderBy: string[], order: string) : Observable<PagedResult<UserInfo>> {
-      console.log("Retrieving data " + searchTerm + "," + offset + "," + limit + "," + orderBy + "," + order);
+      // console.log("Retrieving data " + searchTerm + "," + offset + "," + limit + "," + orderBy + "," + order);
       return userService.query(searchTerm, offset, limit, orderBy, order);
     }
 
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/users/manage-users-roles/manage-users-roles.component.html b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/users/manage-users-roles/manage-users-roles.component.html
index 279e552..a0ba87c 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/users/manage-users-roles/manage-users-roles.component.html
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/users/manage-users-roles/manage-users-roles.component.html
@@ -22,7 +22,7 @@
 
     <div class="row col-md-6">
     <h4 class="col-md-2 mt-3">{{'users.roles.base_roles'|translate}} </h4>
-    <h4 class="col-md-2 offset-md-4 mt-3"><span class="badge badge-primary">{{userid}}</span></h4>
+    <h4 class="col-md-2 offset-md-4 mt-3" *ngIf="roles$"><span class="badge badge-primary">{{userid}}</span></h4>
     </div>
     <ng-container *ngIf="roles$|async as myRoles">
         <table class="table col-md-12">
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/users/manage-users/manage-users.component.ts b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/users/manage-users/manage-users.component.ts
index e33bdcd..8ad6c3e 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/users/manage-users/manage-users.component.ts
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/users/manage-users/manage-users.component.ts
@@ -44,7 +44,6 @@ export class ManageUsersComponent implements OnInit {
     // console.log("Activating "+componentReference+" - "+JSON.stringify(componentReference,getCircularReplacer()))
     if (componentReference.userIdEvent!=null) {
       let componentEmit : Observable<string> = componentReference.userIdEvent.pipe(
-          tap(userid=>console.log("Event "+componentReference.class+" "+userid)),
           map((userid: string) => this.getSubPath(userid)));
       if (this.userId$!=null) {
         this.userId$ = merge(this.userId$, componentEmit)
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/paginated-entities/paginated-entities.component.ts b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/paginated-entities/paginated-entities.component.ts
index d2157d8..8e2e166 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/paginated-entities/paginated-entities.component.ts
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/paginated-entities/paginated-entities.component.ts
@@ -16,7 +16,7 @@
  * under the License.
  */
 
-import {AfterViewInit, Component, EventEmitter, Input, OnInit, Output} from '@angular/core';
+import {AfterViewInit, ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output} from '@angular/core';
 import {concat, merge, Observable, of, pipe, Subject} from "rxjs";
 import {
     concatAll,
@@ -29,7 +29,8 @@ import {
     pluck,
     share,
     startWith,
-    switchMap
+    switchMap,
+    tap
 } from "rxjs/operators";
 import {EntityService} from "../../../model/entity-service";
 import {FieldToggle} from "../../../model/field-toggle";
@@ -64,6 +65,8 @@ import {PagedResult} from "@app/model/paged-result";
 })
 export class PaginatedEntitiesComponent<T> implements OnInit, FieldToggle, AfterViewInit {
 
+    @Input() id: string;
+
     /**
      * This must be set, if you use the component. This service retrieves the entity data.
      */
@@ -138,26 +141,30 @@ export class PaginatedEntitiesComponent<T> implements OnInit, FieldToggle, After
     /**
      * The total number of elements available for the given search term
      */
-    total$: Observable<number>;
+    public total$: Observable<number>;
     /**
      * The entity items retrieved from the service
      */
-    items$: Observable<LoadingValue<PagedResult<T>>>;
+    public items$: Observable<LoadingValue<PagedResult<T>>>;
 
     /**
      * true, if the current page result value represents a result with multiple pages,
      * otherwise false.
      */
-    multiplePages$:Observable<boolean>;
-
+    public multiplePages$:Observable<boolean>;
 
     private pageStream: Subject<number> = new Subject<number>();
     private searchTermStream: Subject<string> = new Subject<string>();
 
     constructor() {
+        // console.log("Construct " + this.id);
+        this.items$=null;
+        this.total$=null;
+        this.multiplePages$=null;
     }
 
     ngOnInit(): void {
+        // console.log("Pag Init " + this.id);
         // We combine the sources for the page and the search input field to a observable 'source'
         const pageSource = this.pageStream.pipe(map(pageNumber => {
             return new PageQuery(this.searchTerm, pageNumber);
@@ -177,10 +184,13 @@ export class PaginatedEntitiesComponent<T> implements OnInit, FieldToggle, After
                     this.service(params.search, (params.page - 1) * this.pageSize, this.pageSize, this.sortField, this.sortOrder)
                         .pipe(map(pagedResult=>LoadingValue.finish<PagedResult<T>>(pagedResult)))
                 )
-            ), share());
-        this.total$ = source.pipe(filter(val=>val.hasValue()),map(val=>val.value),pluck('pagination', 'total_count'));
+            )
+            );
+        this.total$ = source.pipe(filter(val=>val.hasValue()),map(val=>val.value),
+            pluck('pagination', 'total_count'));
+        this.multiplePages$ = source.pipe(filter(val => val.hasValue()),
+            map(val => val.value.pagination.total_count > val.value.pagination.limit));
         this.items$ = source;
-        this.multiplePages$ = source.pipe(filter(val => val.hasValue()), map(val => val.value.pagination.total_count >= val.value.pagination.limit));
     }
 
     search(terms: string) {
@@ -189,7 +199,7 @@ export class PaginatedEntitiesComponent<T> implements OnInit, FieldToggle, After
         this.searchTermStream.next(terms)
     }
 
-    changePage(pageNumber: number) {
+    public changePage(pageNumber: number) {
         // console.log("Page change " +pageNumber);
         this.pageChange.emit(pageNumber);
         this.pageStream.next(pageNumber);
@@ -242,9 +252,16 @@ export class PaginatedEntitiesComponent<T> implements OnInit, FieldToggle, After
     }
 
     ngAfterViewInit(): void {
+        // console.log("Pag afterViewInit " + this.id);
         // We emit the current value to push them to the containing reading components
         this.sortOrderChange.emit(this.sortOrder);
         this.sortFieldChange.emit(this.sortField);
     }
 
+
+    public changeService(newService : EntityService<T>): void {
+        this.service = newService;
+        this.changePage(1);
+    }
+
 }
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/services/role.service.ts b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/services/role.service.ts
index aeabc33..e5b7e39 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/services/role.service.ts
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/services/role.service.ts
@@ -80,6 +80,17 @@ export class RoleService {
     });
   }
 
+  /**
+   * Query for assigned users, that are part of the parent roles.
+   *
+   * @param roleId
+   * @param searchTerm
+   * @param offset
+   * @param limit
+   * @param orderBy
+   * @param order
+   * @param parentsOnly
+   */
   public queryAssignedParentUsers(roleId: string,
                             searchTerm: string, offset: number = 0, limit: number = 5,
                             orderBy: string[] = ['id'], order: string = 'asc', parentsOnly:boolean=true): Observable<PagedResult<User>> {
@@ -100,6 +111,25 @@ export class RoleService {
     });
   }
 
+  public queryUnAssignedUsers(roleId: string,
+                            searchTerm: string, offset: number = 0, limit: number = 5,
+                            orderBy: string[] = ['id'], order: string = 'asc'): Observable<PagedResult<User>> {
+    if (searchTerm == null) {
+      searchTerm = ""
+    }
+    if (orderBy == null || orderBy.length == 0) {
+      orderBy = ['id'];
+    }
+    return this.rest.executeRestCall<PagedResult<User>>("get", "redback", "roles/" + roleId + "/unassigned", {
+      'q': searchTerm,
+      'offset': offset,
+      'limit': limit,
+      'orderBy': orderBy,
+      'order': order
+    });
+  }
+
+
   public getRole(roleId:string) : Observable<Role> {
     return this.rest.executeRestCall("get", "redback", "roles/" + roleId, null);
   }


[archiva] 03/04: Adding user role assignment

Posted by ma...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

martin_s pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/archiva.git

commit b247e09a9e06a92b3d304950276834ebba48c0e2
Author: Martin Stockhammer <ma...@apache.org>
AuthorDate: Mon Dec 21 21:38:49 2020 +0100

    Adding user role assignment
---
 .../manage-roles-edit.component.html               | 179 ++++++++++++++++-----
 .../manage-roles-edit.component.ts                 |  65 +++++++-
 .../manage-users-list.component.html               |  10 +-
 .../paginated-entities.component.html              |  10 +-
 .../paginated-entities.component.ts                |  28 ++++
 .../src/app/modules/shared/shared.module.ts        |   9 +-
 .../archiva-web/src/app/services/role.service.ts   |  39 +++++
 .../src/main/archiva-web/src/assets/i18n/en.json   |  12 +-
 8 files changed, 297 insertions(+), 55 deletions(-)

diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/roles/manage-roles-edit/manage-roles-edit.component.html b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/roles/manage-roles-edit/manage-roles-edit.component.html
index b146c72..bf6dd1e 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/roles/manage-roles-edit/manage-roles-edit.component.html
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/roles/manage-roles-edit/manage-roles-edit.component.html
@@ -18,9 +18,9 @@
 
 <form class="mt-3 mb-3" [formGroup]="userForm" (ngSubmit)="onSubmit()">
     <div class="form-group row col-md-8">
-        <div class="col-md-1" >{{'form.edit' |translate}}&nbsp;<span
+        <div class="col-md-1">{{'form.edit' |translate}}&nbsp;<span
                 class="fas fa-edit"></span></div>
-        <div class="col-md-6" >
+        <div class="col-md-6">
             <input class="form-check-input" type="checkbox" [value]="editMode" [checked]="editMode"
                    (change)="editMode=!editMode"
             >
@@ -75,14 +75,14 @@
             </div>
         </div>
         <div class="col-md-2" *ngIf="editMode">
-           <button class="btn btn-primary" type="submit"
-            [disabled]="userForm.invalid || !userForm.dirty">{{'form.button.save'|translate}}</button>
+            <button class="btn btn-primary" type="submit"
+                    [disabled]="userForm.invalid || !userForm.dirty">{{'form.button.save'|translate}}</button>
         </div>
     </div>
 </form>
 <hr/>
 <ngb-accordion activeIds="parents,children,permissions">
-    <ngb-panel id="parents" >
+    <ngb-panel id="parents">
         <ng-template ngbPanelHeader let-opened="opened">
             <div class="d-flex align-items-center justify-content-between">
                 <button ngbPanelToggle class="btn btn-link text-left shadow-none">
@@ -92,27 +92,28 @@
             </div>
         </ng-template>
         <ng-template ngbPanelContent>
-                <ng-container *ngIf="editRole?.parents.length>0">
-                    <ul class="list-group" *ngFor="let parentRole of editRole?.parents">
-                        <li class="list-group-item"><a routerLink="../{{parentRole?.id}}">{{parentRole?.name}}</a></li>
-                    </ul>
-                </ng-container>
+            <ng-container *ngIf="editRole?.parents.length>0">
+                <ul class="list-group" *ngFor="let parentRole of editRole?.parents">
+                    <li class="list-group-item"><a routerLink="../{{parentRole?.id}}">{{parentRole?.name}}</a></li>
+                </ul>
+            </ng-container>
         </ng-template>
     </ngb-panel>
     <ngb-panel id="children">
         <ng-template ngbPanelHeader let-opened="opened">
             <div class="d-flex align-items-center justify-content-between">
-            <button ngbPanelToggle class="btn btn-link text-left shadow-none"><h3>{{'roles.edit.children'|translate}}</h3></button>
-            <ng-container *ngIf="!opened"><i class="fa fa-eye-slash"></i></ng-container>
-            <ng-container *ngIf="opened"><i class="fa fa-eye"></i></ng-container>
+                <button ngbPanelToggle class="btn btn-link text-left shadow-none">
+                    <h3>{{'roles.edit.children'|translate}}</h3></button>
+                <ng-container *ngIf="!opened"><i class="fa fa-eye-slash"></i></ng-container>
+                <ng-container *ngIf="opened"><i class="fa fa-eye"></i></ng-container>
             </div>
         </ng-template>
         <ng-template ngbPanelContent>
-                <ng-container *ngIf="editRole?.children.length>0">
-                    <ul class="list-group" *ngFor="let childRole of editRole?.children">
-                        <li class="list-group-item"><a routerLink="../{{childRole?.id}}">{{childRole?.name}}</a></li>
-                    </ul>
-                </ng-container>
+            <ng-container *ngIf="editRole?.children.length>0">
+                <ul class="list-group" *ngFor="let childRole of editRole?.children">
+                    <li class="list-group-item"><a routerLink="../{{childRole?.id}}">{{childRole?.name}}</a></li>
+                </ul>
+            </ng-container>
         </ng-template>
     </ngb-panel>
 
@@ -120,44 +121,138 @@
         <ng-template ngbPanelHeader let-opened="opened">
             <div class="d-flex align-items-center justify-content-between">
 
-            <button ngbPanelToggle class="btn btn-link text-left shadow-none"><h3>{{'roles.edit.permissions'|translate}}</h3></button>
-            <ng-container *ngIf="!opened"><i class="fa fa-eye-slash"></i></ng-container>
-            <ng-container *ngIf="opened"><i class="fa fa-eye"></i></ng-container>
+                <button ngbPanelToggle class="btn btn-link text-left shadow-none">
+                    <h3>{{'roles.edit.permissions'|translate}}</h3></button>
+                <ng-container *ngIf="!opened"><i class="fa fa-eye-slash"></i></ng-container>
+                <ng-container *ngIf="opened"><i class="fa fa-eye"></i></ng-container>
             </div>
         </ng-template>
         <ng-template ngbPanelContent>
-                <ng-container *ngIf="editRole">
-                    <table class="table">
-                        <thead>
-                        <tr>
-                            <th>{{'permissions.attributes.permission'|translate}}</th>
-                            <th>{{'permissions.attributes.operation'|translate}}</th>
-                            <th>{{'permissions.attributes.resource'|translate}}</th>
-                        </tr>
-                        </thead>
-                        <tbody *ngFor="let perm of editRole.permissions">
-                        <tr>
-                            <td>{{perm.name}}</td>
-                            <td>{{perm.operation.name}}</td>
-                            <td>{{perm.resource.identifier}}</td>
-                        </tr>
-                        </tbody>
+            <ng-container *ngIf="editRole">
+                <table class="table">
+                    <thead>
+                    <tr>
+                        <th>{{'permissions.attributes.permission'|translate}}</th>
+                        <th>{{'permissions.attributes.operation'|translate}}</th>
+                        <th>{{'permissions.attributes.resource'|translate}}</th>
+                    </tr>
+                    </thead>
+                    <tbody *ngFor="let perm of editRole.permissions">
+                    <tr>
+                        <td>{{perm.name}}</td>
+                        <td>{{perm.operation.name}}</td>
+                        <td>{{perm.resource.identifier}}</td>
+                    </tr>
+                    </tbody>
 
-                    </table>
-                </ng-container>
+                </table>
+            </ng-container>
         </ng-template>
     </ngb-panel>
 
     <ngb-panel>
         <ng-template ngbPanelHeader let-opened="opened">
             <div class="d-flex align-items-center justify-content-between">
-                <button ngbPanelToggle class="btn btn-link text-left shadow-none"><h3>{{'roles.edit.users'|translate}}</h3></button>
+                <button ngbPanelToggle class="btn btn-link text-left shadow-none">
+                    <h3>{{'roles.edit.users'|translate}}</h3></button>
                 <ng-container *ngIf="!opened"><i class="fa fa-eye-slash"></i></ng-container>
                 <ng-container *ngIf="opened"><i class="fa fa-eye"></i></ng-container>
             </div>
         </ng-template>
-        <ng-template ngbPanelContent >
-             <h3>There are the users</h3>
+        <ng-template ngbPanelContent>
+            <h4>{{'roles.edit.usersParents'|translate}}</h4>
+            <app-paginated-entities [service]="roleUserParentService" pageSize="5" [(sortField)]="userParentSortField"
+                                    [(sortOrder)]="userParentSortOrder" [displayControlsIfSinglePage]="false"
+                                    #userParentSection>
+
+                <ng-container *ngIf="userParentSection.items$ |async as itemLoader">
+                    <ng-template [ngIf]="itemLoader.loading">
+                        <div class="spinner-border text-primary" role="status">
+                            <span class="sr-only">Loading...</span>
+                        </div>
+                    </ng-template>
+                </ng-container>
+                <ng-container *ngIf="userParentSection.items$ |stripLoading|async as userItem">
+                    <table class="table table-striped table-bordered">
+                        <thead class="thead-light">
+                        <tr sorted [sortFieldEmitter]="userParentSection.sortFieldChange"
+                            [sortOrderEmitter]="userParentSection.sortOrderChange"
+                            [toggleObserver]="userParentSection">
+                            <app-th-sorted [fieldArray]="['user_id']"
+                                           contentText="users.attributes.user_id"></app-th-sorted>
+                            <app-th-sorted [fieldArray]="['full_name']"
+                                           contentText="users.attributes.full_name"></app-th-sorted>
+                        </tr>
+                        </thead>
+                        <tbody>
+                        <tr *ngFor="let user  of userItem.data">
+                            <td><span data-toggle="tooltip" placement="left"
+                                      ngbTooltip="{{user.id}}">{{user.user_id}}</span>
+                            </td>
+                            <td>{{user.full_name}}</td>
+                        </tr>
+                        </tbody>
+                    </table>
+                </ng-container>
+            </app-paginated-entities>
+            <hr/>
+            <h4>{{'roles.edit.usersInstance'|translate}}</h4>
+            <app-paginated-entities [service]="roleUserService" pageSize="5" [(sortField)]="userSortField"
+                                    [(sortOrder)]="userSortOrder"
+                                    [displayIfEmpty]="false" [displayKeyIfEmpty]="'roles.edit.noUsersAssigned'"
+                                    #userSection>
+
+                <ng-container *ngIf="userSection.items$ |async as itemLoader">
+                    <ng-template [ngIf]="itemLoader.loading">
+                        <div class="spinner-border text-primary" role="status">
+                            <span class="sr-only">Loading...</span>
+                        </div>
+                    </ng-template>
+                </ng-container>
+                <ng-container *ngIf="userSection.items$ |stripLoading|async as userItem">
+                    <table class="table table-striped table-bordered">
+                        <thead class="thead-light">
+                        <tr sorted [sortFieldEmitter]="userSection.sortFieldChange"
+                            [sortOrderEmitter]="userSection.sortOrderChange"
+                            [toggleObserver]="userSection">
+                            <app-th-sorted [fieldArray]="['user_id']"
+                                           contentText="users.attributes.user_id"></app-th-sorted>
+                            <app-th-sorted [fieldArray]="['full_name']"
+                                           contentText="users.attributes.full_name"></app-th-sorted>
+                        </tr>
+                        </thead>
+                        <tbody>
+                        <tr *ngFor="let user  of userItem.data">
+                            <td><span data-toggle="tooltip" placement="left"
+                                      ngbTooltip="{{user.id}}">{{user.user_id}}</span>
+                            </td>
+                            <td>{{user.full_name}}</td>
+                        </tr>
+                        </tbody>
+                    </table>
+                </ng-container>
+            </app-paginated-entities>
+            <hr/>
+            <form class="mt-2">
+                <ng-template #userResultTemplate let-r="result" let-t="term">
+                    <ngb-highlight [result]="r.user_id + '-' + r.full_name" [term]="t"></ngb-highlight>
+                </ng-template>
+                <div class="form-group">
+                    <label for="typeahead-http">{{'roles.edit.assignUserSearch'|translate}}</label>
+                    <input id="typeahead-http" type="text" class="form-control col-md-2"
+                           name="userSearchField"
+                           [class.is-invalid]="userSearchFailed" [resultTemplate]="userResultTemplate"
+                           [inputFormatter]="getUserId"
+                           [placement]="'top'"
+                           [(ngModel)]="userSearchModel" [ngbTypeahead]="searchUser" placeholder="User Search"/>
+                    <small *ngIf="userSearching"
+                           class="form-text text-muted">{{'form.searching'  |translate}}</small>
+                    <div class="invalid-feedback"
+                         *ngIf="userSearchFailed">{{'roles.edit.userSearchFailed'|translate}}</div>
+                </div>
+                <button class="btn btn-primary" (click)="assignUserRole()">{{'roles.edit.assignButton'|translate}}</button>
+            </form>
+
         </ng-template>
     </ngb-panel>
 
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/roles/manage-roles-edit/manage-roles-edit.component.ts b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/roles/manage-roles-edit/manage-roles-edit.component.ts
index b5d033f..71655fe 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/roles/manage-roles-edit/manage-roles-edit.component.ts
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/roles/manage-roles-edit/manage-roles-edit.component.ts
@@ -20,12 +20,17 @@ import {AfterContentInit, Component, EventEmitter, OnInit, Output} from '@angula
 import {ActivatedRoute} from "@angular/router";
 import {FormBuilder, Validators} from "@angular/forms";
 import {RoleService} from "@app/services/role.service";
-import {catchError, filter, map, switchMap, tap} from "rxjs/operators";
+import {catchError, debounceTime, distinctUntilChanged, filter, map, switchMap, tap} from "rxjs/operators";
 import {Role} from '@app/model/role';
 import {ErrorResult} from "@app/model/error-result";
 import {EditBaseComponent} from "@app/modules/shared/edit-base.component";
-import {forkJoin, iif, Observable, of, pipe, zip} from 'rxjs';
+import {forkJoin, Observable, of, zip} from 'rxjs';
 import {RoleUpdate} from "@app/model/role-update";
+import {EntityService} from "@app/model/entity-service";
+import {User} from '@app/model/user';
+import {PagedResult} from "@app/model/paged-result";
+import {UserService} from "@app/services/user.service";
+import {UserInfo} from '@app/model/user-info';
 
 @Component({
     selector: 'app-manage-roles-edit',
@@ -38,12 +43,23 @@ export class ManageRolesEditComponent extends EditBaseComponent<Role> implements
     editProperties = ['id', 'name', 'description', 'template_instance', 'resource', 'assignable'];
     originRole;
     roleCache: Map<string, Role> = new Map<string, Role>();
+    roleUserService: EntityService<User>
+    roleUserParentService: EntityService<User>;
+    userSortField = ["id"];
+    userSortOrder = "asc";
+    userParentSortField = ["id"];
+    userParentSortOrder = "asc";
+
+    userSearching:boolean=false;
+    userSearchFailed:boolean=false;
+
+    public userSearchModel:any;
 
 
     @Output()
     roleIdEvent: EventEmitter<string> = new EventEmitter<string>(true);
 
-    constructor(private route: ActivatedRoute, private roleService: RoleService, public fb: FormBuilder) {
+    constructor(private route: ActivatedRoute, public roleService: RoleService, private userService: UserService, public fb: FormBuilder) {
         super(fb);
         super.init(fb.group({
             id: [''],
@@ -53,6 +69,7 @@ export class ManageRolesEditComponent extends EditBaseComponent<Role> implements
             template_instance: [''],
             assignable: ['']
         }, {}));
+
     }
 
     createEntity(): Role {
@@ -75,6 +92,14 @@ export class ManageRolesEditComponent extends EditBaseComponent<Role> implements
             this.editRole = role;
             this.originRole = role;
             this.copyToForm(this.editProperties, this.editRole);
+            const fRoleService = this.roleService;
+            const roleId = role.id;
+            this.roleUserService = function (searchTerm: string, offset: number, limit: number, orderBy: string[], order: string): Observable<PagedResult<User>> {
+                return fRoleService.queryAssignedUsers(roleId, searchTerm, offset, limit, orderBy, order);
+            };
+            this.roleUserParentService = function (searchTerm: string, offset: number, limit: number, orderBy: string[], order: string): Observable<PagedResult<User>> {
+                return fRoleService.queryAssignedParentUsers(roleId, searchTerm, offset, limit, orderBy, order, true);
+            };
         }, error => {
             this.editRole = new Role();
         });
@@ -165,5 +190,39 @@ export class ManageRolesEditComponent extends EditBaseComponent<Role> implements
         }
     }
 
+    searchUser = (text$: Observable<string>) =>
+        text$.pipe(
+            debounceTime(300),
+            distinctUntilChanged(),
+            tap(() => this.userSearching = true),
+            switchMap(term =>
+                this.userService.query(term, 0, 10).pipe(
+                    tap(() => this.userSearchFailed = false),
+                    map(pagedResult=>
+                    pagedResult.data),
+                    catchError(() => {
+                        this.userSearchFailed = true;
+                        return of([]);
+                    }))
+            ),
+            tap(() => this.userSearching = false)
+        )
+
+    getUserId(item:UserInfo) : string {
+        return item.user_id;
+    }
+
+    assignUserRole() {
+        let userId;
+        if (typeof(this.userSearchModel)=='string') {
+            userId=this.userSearchModel;
+        } else {
+            if (this.userSearchModel.user_id) {
+                userId = this.userSearchModel.user_id;
+            }
+        }
+        console.log("Assigning user " + userId)
+    }
+
 }
 
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/users/manage-users-list/manage-users-list.component.html b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/users/manage-users-list/manage-users-list.component.html
index d87fc57..f72b3e7 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/users/manage-users-list/manage-users-list.component.html
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/users/manage-users-list/manage-users-list.component.html
@@ -19,14 +19,14 @@
 <app-paginated-entities [service]="service" pageSize="10" [(sortField)]="sortField" [(sortOrder)]="sortOrder"
                         #parent>
 
-    <ng-container *ngIf="parent.items$ |async as roleItemLoader">
-        <ng-template [ngIf]="roleItemLoader.loading">
+    <ng-container *ngIf="parent.items$ |async as itemLoader">
+        <ng-template [ngIf]="itemLoader.loading">
             <div class="spinner-border text-primary" role="status">
                 <span class="sr-only">Loading...</span>
             </div>
         </ng-template>
     </ng-container>
-    <ng-container *ngIf="parent.items$ |stripLoading|async as roleItem">
+    <ng-container *ngIf="parent.items$ |stripLoading|async as userItem">
 
             <table class="table table-striped table-bordered">
                 <thead class="thead-light">
@@ -54,11 +54,11 @@
                     <app-th-sorted [fieldArray]="['created']" contentText="users.attributes.created"></app-th-sorted>
                     <app-th-sorted [fieldArray]="['last_password_change']"
                                    contentText="users.attributes.last_password_change"></app-th-sorted>
-                    <th>Action</th>
+                    <th>{{'headers.action'|translate}}</th>
                 </tr>
                 </thead>
                 <tbody>
-                <tr *ngFor="let user  of roleItem.data"
+                <tr *ngFor="let user  of userItem.data"
                     [ngClass]="(user.permanent||user.readOnly)?'table-secondary':''">
                     <td><span data-toggle="tooltip" placement="left" ngbTooltip="{{user.id}}">{{user.user_id}}</span>
                     </td>
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/paginated-entities/paginated-entities.component.html b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/paginated-entities/paginated-entities.component.html
index ce0a6a5..879ff98 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/paginated-entities/paginated-entities.component.html
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/paginated-entities/paginated-entities.component.html
@@ -16,8 +16,9 @@
   ~ under the License.
   -->
 
+<ng-template [ngIf]="(displayIfEmpty || (total$|async)>0)" [ngIfElse]="noContent" >
 <form class="mt-3 mb-3">
-    <div class="form-row align-items-center">
+    <div class="form-row align-items-center" *ngIf="displayControlsIfSinglePage||(multiplePages$|async)">
         <div class="col-lg-4 col-md-2 col-sm-1">
             <label class="sr-only" for="searchQuery">{{'search.label' |translate}}</label>
             <input type="text" class="form-control" id="searchQuery" placeholder="{{'search.input'|translate}}" #searchTerm
@@ -33,6 +34,11 @@
 
 <ng-content></ng-content>
 
-<ngb-pagination [collectionSize]="total$|async" [pageSize]="pageSize" [maxSize]="pagination.maxSize" [rotate]="pagination.rotate"
+<ngb-pagination *ngIf="displayControlsIfSinglePage||(multiplePages$|async)"
+                [collectionSize]="total$|async" [pageSize]="pageSize" [maxSize]="pagination.maxSize" [rotate]="pagination.rotate"
                 [boundaryLinks]="pagination.boundaryLinks" [ellipses]="pagination.ellipses"
                 [(page)]="page" (pageChange)="changePage($event)" aria-label="Pagination"></ngb-pagination>
+</ng-template>
+<ng-template #noContent>
+    {{displayKeyIfEmpty|translate}}
+</ng-template>
\ No newline at end of file
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/paginated-entities/paginated-entities.component.ts b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/paginated-entities/paginated-entities.component.ts
index cc62ac6..d2157d8 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/paginated-entities/paginated-entities.component.ts
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/paginated-entities/paginated-entities.component.ts
@@ -94,6 +94,26 @@ export class PaginatedEntitiesComponent<T> implements OnInit, FieldToggle, After
     }
 
     /**
+     * If true, all controls are displayed, if the total count is 0
+     */
+    @Input()
+    displayIfEmpty:boolean=true;
+    /**
+     * Sets the translation key, for the text to display, if displayIfEmpty=false and the total count is 0.
+     */
+    @Input()
+    displayKeyIfEmpty:string='form.emptyContent';
+
+    /**
+     * If set to true, all controls are displayed, even if there is only one page to display.
+     * Otherwise the controls are not displayed, if there is only a single page of results.
+     */
+    @Input()
+    displayControlsIfSinglePage:boolean=true;
+
+
+
+    /**
      * The current page that is selected
      */
     page = 1;
@@ -124,6 +144,13 @@ export class PaginatedEntitiesComponent<T> implements OnInit, FieldToggle, After
      */
     items$: Observable<LoadingValue<PagedResult<T>>>;
 
+    /**
+     * true, if the current page result value represents a result with multiple pages,
+     * otherwise false.
+     */
+    multiplePages$:Observable<boolean>;
+
+
     private pageStream: Subject<number> = new Subject<number>();
     private searchTermStream: Subject<string> = new Subject<string>();
 
@@ -153,6 +180,7 @@ export class PaginatedEntitiesComponent<T> implements OnInit, FieldToggle, After
             ), share());
         this.total$ = source.pipe(filter(val=>val.hasValue()),map(val=>val.value),pluck('pagination', 'total_count'));
         this.items$ = source;
+        this.multiplePages$ = source.pipe(filter(val => val.hasValue()), map(val => val.value.pagination.total_count >= val.value.pagination.limit));
     }
 
     search(terms: string) {
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/shared.module.ts b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/shared.module.ts
index 0420576..76bd984 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/shared.module.ts
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/shared.module.ts
@@ -21,7 +21,13 @@ import {CommonModule} from '@angular/common';
 import {PaginatedEntitiesComponent} from "./paginated-entities/paginated-entities.component";
 import {SortedTableHeaderComponent} from "./sorted-table-header/sorted-table-header.component";
 import {SortedTableHeaderRowComponent} from "./sorted-table-header-row/sorted-table-header-row.component";
-import {NgbAccordionModule, NgbModalModule, NgbPaginationModule, NgbTooltipModule} from "@ng-bootstrap/ng-bootstrap";
+import {
+    NgbAccordionModule,
+    NgbModalModule,
+    NgbPaginationModule,
+    NgbTooltipModule,
+    NgbTypeaheadModule
+} from "@ng-bootstrap/ng-bootstrap";
 import {TranslateCompiler, TranslateLoader, TranslateModule} from "@ngx-translate/core";
 import {TranslateMessageFormatCompiler} from "ngx-translate-messageformat-compiler";
 import {HttpClient} from "@angular/common/http";
@@ -49,6 +55,7 @@ export { PageQuery } from './model/page-query';
         NgbTooltipModule,
         NgbAccordionModule,
         NgbModalModule,
+        NgbTypeaheadModule,
         PaginatedEntitiesComponent,
         SortedTableHeaderComponent,
         SortedTableHeaderRowComponent,
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/services/role.service.ts b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/services/role.service.ts
index 99424a0..aeabc33 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/services/role.service.ts
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/services/role.service.ts
@@ -25,6 +25,7 @@ import {HttpResponse} from "@angular/common/http";
 import {PagedResult} from "@app/model/paged-result";
 import {UserInfo} from "@app/model/user-info";
 import {RoleUpdate} from "@app/model/role-update";
+import { User } from '@app/model/user';
 
 @Injectable({
   providedIn: 'root'
@@ -61,6 +62,44 @@ export class RoleService {
     });
   }
 
+  public queryAssignedUsers(roleId: string,
+                            searchTerm: string, offset: number = 0, limit: number = 5,
+                            orderBy: string[] = ['id'], order: string = 'asc'): Observable<PagedResult<User>> {
+    if (searchTerm == null) {
+      searchTerm = ""
+    }
+    if (orderBy == null || orderBy.length == 0) {
+      orderBy = ['id'];
+    }
+    return this.rest.executeRestCall<PagedResult<User>>("get", "redback", "roles/" + roleId + "/user", {
+      'q': searchTerm,
+      'offset': offset,
+      'limit': limit,
+      'orderBy': orderBy,
+      'order': order
+    });
+  }
+
+  public queryAssignedParentUsers(roleId: string,
+                            searchTerm: string, offset: number = 0, limit: number = 5,
+                            orderBy: string[] = ['id'], order: string = 'asc', parentsOnly:boolean=true): Observable<PagedResult<User>> {
+    if (searchTerm == null) {
+      searchTerm = ""
+    }
+    if (orderBy == null || orderBy.length == 0) {
+      orderBy = ['id'];
+    }
+    const recurseFlag = parentsOnly ? 'parentsOnly' : 'true';
+    return this.rest.executeRestCall<PagedResult<User>>("get", "redback", "roles/" + roleId + "/user", {
+      'recurse':recurseFlag,
+      'q': searchTerm,
+      'offset': offset,
+      'limit': limit,
+      'orderBy': orderBy,
+      'order': order
+    });
+  }
+
   public getRole(roleId:string) : Observable<Role> {
     return this.rest.executeRestCall("get", "redback", "roles/" + roleId, null);
   }
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/assets/i18n/en.json b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/assets/i18n/en.json
index d094e39..297c9f2 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/assets/i18n/en.json
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/assets/i18n/en.json
@@ -136,7 +136,13 @@
       "parents": "Parents",
       "children": "Children",
       "permissions": "Permissions",
-      "users": "Users"
+      "users": "Users",
+      "usersInstance": "Users Assigned to this Role",
+      "usersParents": "Users Assigned to Parent Roles",
+      "noUsersAssigned": "There are no users assigned to this role",
+      "assignUserSearch": "Search and assign users to this role",
+      "userSearchFailed": "Sorry, could not load users",
+      "assignButton": "Assign"
     },
     "attributes": {
       "id": "Identifier",
@@ -172,7 +178,9 @@
       "no": "No",
       "save": "Save Changes"
     },
-    "edit": "Edit"
+    "edit": "Edit",
+    "emptyContent": "No values",
+    "searching": "Searching ..."
   },
   "headers": {
     "action": "Action"


[archiva] 01/04: Adding role information

Posted by ma...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

martin_s pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/archiva.git

commit 130e4a35e44c99fe0c6f6da95d21b76bcf8db1fc
Author: Martin Stockhammer <ma...@apache.org>
AuthorDate: Fri Dec 18 21:29:24 2020 +0100

    Adding role information
---
 .../src/main/archiva-web/src/app/model/role.ts     |   6 +
 .../app/modules/security/role-routing.module.ts    |   1 +
 .../manage-roles-edit.component.html               | 144 ++++++++++++++++++++-
 .../manage-roles-edit.component.ts                 | 142 ++++++++++++++++++--
 .../manage-roles-list.component.html               |   3 +
 .../manage-roles-list.component.ts                 |  11 +-
 .../roles/manage-roles/manage-roles.component.ts   |   1 -
 .../manage-users-edit.component.html               |   2 +-
 .../src/app/modules/shared/edit-base.component.ts  | 133 +++++++++++++++++++
 .../src/app/modules/shared/shared.module.ts        |   4 +-
 .../app/modules/shared/sorted-table-component.ts   |  11 ++
 .../archiva-web/src/app/services/role.service.ts   |   9 +-
 .../src/main/archiva-web/src/assets/i18n/en.json   |  20 ++-
 13 files changed, 465 insertions(+), 22 deletions(-)

diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/model/role.ts b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/model/role.ts
index 819d0f1..ac0b469 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/model/role.ts
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/model/role.ts
@@ -16,6 +16,8 @@
  * under the License.
  */
 
+import {Permission} from "@app/model/permission";
+
 export class Role {
     id: string
     name: string
@@ -29,7 +31,11 @@ export class Role {
     model_id:string
     resource:string
 
+    child_role_ids: Array<string>
+    parent_role_ids: Array<string>
     children: Array<Role>
+    parents: Array<Role>
+    permissions: Array<Permission>
 
     // Web Internal attributes
     enabled: boolean = true
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/role-routing.module.ts b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/role-routing.module.ts
index 6452e65..1be49d7 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/role-routing.module.ts
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/role-routing.module.ts
@@ -35,6 +35,7 @@ const routes: Routes = [
         children: [
             {path: 'list', component: ManageRolesListComponent},
             {path: 'edit/:roleid', component: ManageRolesEditComponent},
+            {path: 'edit', component: ManageRolesEditComponent},
             {path: '', redirectTo: 'list', pathMatch: 'full'}
         ]
     }
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/roles/manage-roles-edit/manage-roles-edit.component.html b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/roles/manage-roles-edit/manage-roles-edit.component.html
index 292d9d5..d92d21c 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/roles/manage-roles-edit/manage-roles-edit.component.html
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/roles/manage-roles-edit/manage-roles-edit.component.html
@@ -16,4 +16,146 @@
   ~ under the License.
   -->
 
-<p>manage-roles-edit works!</p>
+<form class="mt-3 mb-3" [formGroup]="userForm" (ngSubmit)="onSubmit()">
+    <div class="form-group row col-md-8">
+        <div class="col-md-1" *ngIf="editRole && !editRole.permanent">{{'form.edit' |translate}}&nbsp;<span
+                class="fas fa-edit"></span></div>
+        <div class="col-md-6" *ngIf="editRole && !editRole.permanent">
+            <input class="form-check-input" type="checkbox" [value]="editMode" [checked]="editMode"
+                   (change)="editMode=!editMode"
+            >
+        </div>
+    </div>
+    <div class="form-group row col-md-8">
+        <label class="col-md-2 col-form-label" for="id">{{'roles.attributes.id' |translate}}</label>
+        <div class="col-md-6">
+            <input type="text" formControlName="id" id="id"
+                   [ngClass]="valid('id')"
+                   [attr.readonly]="true">
+        </div>
+    </div>
+    <div class="form-group row col-md-8">
+        <label class="col-md-2 col-form-label" for="id">{{'roles.attributes.name' |translate}}</label>
+        <div class="col-md-6">
+            <input type="text" formControlName="name" id="name"
+                   [ngClass]="valid('name')"
+                   [attr.readonly]="editMode?null:'true'">
+        </div>
+    </div>
+    <div class="form-group row col-md-8">
+        <label class="col-md-2 col-form-label" for="id">{{'roles.attributes.description' |translate}}</label>
+        <div class="col-md-6">
+            <input type="text" formControlName="description" id="description"
+                   [ngClass]="valid('description')"
+                   [attr.readonly]="editMode?null:'true'">
+        </div>
+    </div>
+    <div class="form-group row col-md-8">
+        <label class="col-md-2 col-form-label" for="id">{{'roles.attributes.resource' |translate}}</label>
+        <div class="col-md-6">
+            <input type="text" formControlName="resource" id="resource"
+                   [ngClass]="valid('resource')"
+                   [attr.readonly]="true">
+        </div>
+    </div>
+    <div class="form-group row col-md-8">
+        <div class="col-md-2"></div>
+        <div class="col-md-6">
+            <div class="form-check">
+                <input class="form-check-input" type="checkbox" formControlName="assignable"
+                       id="assignable" [attr.disabled]="true">
+                <label class="form-check-label " for="assignable">
+                    {{'roles.attributes.assignable'|translate}}
+                </label>
+            </div>
+            <div class="form-check">
+                <input class="form-check-input" type="checkbox" formControlName="template_instance"
+                       id="template_instance" [attr.disabled]="true">
+                <label class="form-check-label " for="template_instance">
+                    {{'roles.attributes.template_instance'|translate}}
+                </label>
+            </div>
+        </div>
+    </div>
+</form>
+<hr/>
+<ngb-accordion activeIds="parents,children,permissions">
+    <ngb-panel id="parents" >
+        <ng-template ngbPanelHeader let-opened="opened">
+            <div class="d-flex align-items-center justify-content-between">
+                <button ngbPanelToggle class="btn btn-link text-left shadow-none"><h3>{{'roles.edit.parents'|translate}}</h3></button>
+                <ng-container *ngIf="!opened"><i class="fa fa-eye-slash"></i></ng-container>
+                <ng-container *ngIf="opened"><i class="fa fa-eye"></i></ng-container>
+            </div>
+        </ng-template>
+        <ng-template ngbPanelContent>
+                <ng-container *ngIf="editRole?.parents.length>0">
+                    <ul class="list-group" *ngFor="let parentRole of editRole?.parents">
+                        <li class="list-group-item"><a routerLink="../{{parentRole?.id}}">{{parentRole?.name}}</a></li>
+                    </ul>
+                </ng-container>
+        </ng-template>
+    </ngb-panel>
+    <ngb-panel id="children">
+        <ng-template ngbPanelHeader let-opened="opened">
+            <div class="d-flex align-items-center justify-content-between">
+            <button ngbPanelToggle class="btn btn-link text-left shadow-none"><h3>{{'roles.edit.children'|translate}}</h3></button>
+            <ng-container *ngIf="!opened"><i class="fa fa-eye-slash"></i></ng-container>
+            <ng-container *ngIf="opened"><i class="fa fa-eye"></i></ng-container>
+            </div>
+        </ng-template>
+        <ng-template ngbPanelContent>
+                <ng-container *ngIf="editRole?.children.length>0">
+                    <ul class="list-group" *ngFor="let childRole of editRole?.children">
+                        <li class="list-group-item"><a routerLink="../{{childRole?.id}}">{{childRole?.name}}</a></li>
+                    </ul>
+                </ng-container>
+        </ng-template>
+    </ngb-panel>
+
+    <ngb-panel id="permissions">
+        <ng-template ngbPanelHeader let-opened="opened">
+            <div class="d-flex align-items-center justify-content-between">
+
+            <button ngbPanelToggle class="btn btn-link text-left shadow-none"><h3>{{'roles.edit.permissions'|translate}}</h3></button>
+            <ng-container *ngIf="!opened"><i class="fa fa-eye-slash"></i></ng-container>
+            <ng-container *ngIf="opened"><i class="fa fa-eye"></i></ng-container>
+            </div>
+        </ng-template>
+        <ng-template ngbPanelContent>
+                <ng-container *ngIf="editRole">
+                    <table class="table">
+                        <thead>
+                        <tr>
+                            <th>{{'permissions.attributes.permission'|translate}}</th>
+                            <th>{{'permissions.attributes.operation'|translate}}</th>
+                            <th>{{'permissions.attributes.resource'|translate}}</th>
+                        </tr>
+                        </thead>
+                        <tbody *ngFor="let perm of editRole.permissions">
+                        <tr>
+                            <td>{{perm.name}}</td>
+                            <td>{{perm.operation.name}}</td>
+                            <td>{{perm.resource.identifier}}</td>
+                        </tr>
+                        </tbody>
+
+                    </table>
+                </ng-container>
+        </ng-template>
+    </ngb-panel>
+
+    <ngb-panel>
+        <ng-template ngbPanelHeader let-opened="opened">
+            <div class="d-flex align-items-center justify-content-between">
+                <button ngbPanelToggle class="btn btn-link text-left shadow-none"><h3>{{'roles.edit.users'|translate}}</h3></button>
+                <ng-container *ngIf="!opened"><i class="fa fa-eye-slash"></i></ng-container>
+                <ng-container *ngIf="opened"><i class="fa fa-eye"></i></ng-container>
+            </div>
+        </ng-template>
+        <ng-template ngbPanelContent >
+             <h3>There are the users</h3>
+        </ng-template>
+    </ngb-panel>
+
+</ngb-accordion>
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/roles/manage-roles-edit/manage-roles-edit.component.ts b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/roles/manage-roles-edit/manage-roles-edit.component.ts
index 4a642cb..cbbd5d5 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/roles/manage-roles-edit/manage-roles-edit.component.ts
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/roles/manage-roles-edit/manage-roles-edit.component.ts
@@ -16,18 +16,144 @@
  * under the License.
  */
 
-import { Component, OnInit } from '@angular/core';
+import {Component, EventEmitter, OnInit, Output} from '@angular/core';
+import {ActivatedRoute} from "@angular/router";
+import {FormBuilder, Validators} from "@angular/forms";
+import {RoleService} from "@app/services/role.service";
+import {catchError, filter, map, switchMap, tap} from "rxjs/operators";
+import {Role} from '@app/model/role';
+import {ErrorResult} from "@app/model/error-result";
+import {EditBaseComponent} from "@app/modules/shared/edit-base.component";
+import {forkJoin, iif, Observable, of, pipe, zip} from 'rxjs';
 
 @Component({
-  selector: 'app-manage-roles-edit',
-  templateUrl: './manage-roles-edit.component.html',
-  styleUrls: ['./manage-roles-edit.component.scss']
+    selector: 'app-manage-roles-edit',
+    templateUrl: './manage-roles-edit.component.html',
+    styleUrls: ['./manage-roles-edit.component.scss']
 })
-export class ManageRolesEditComponent implements OnInit {
+export class ManageRolesEditComponent extends EditBaseComponent<Role> implements OnInit {
 
-  constructor() { }
+    parentsOpened: boolean
 
-  ngOnInit(): void {
-  }
+    editRole: Role;
+    editProperties = ['id', 'name', 'description', 'template_instance', 'resource', 'assignable'];
+    originRole;
+    roleCache: Map<string, Role> = new Map<string, Role>();
+
+
+    @Output()
+    roleIdEvent: EventEmitter<string> = new EventEmitter<string>(true);
+
+    constructor(private route: ActivatedRoute, private roleService: RoleService, public fb: FormBuilder) {
+        super(fb);
+        super.init(fb.group({
+            id: ['', [Validators.required]],
+            name: ['', Validators.required],
+            description: [''],
+            resource: [''],
+            template_instance: [''],
+            assignable: ['']
+        }, {}));
+    }
+
+    createEntity(): Role {
+        return new Role();
+    }
+
+    ngOnInit(): void {
+        this.route.params.pipe(
+            map(params => params.roleid),
+            filter(roleid => roleid != null),
+            tap(roleid => {
+                this.roleIdEvent.emit(roleid)
+            }),
+            switchMap((roleid: string) => this.roleService.getRole(roleid)),
+            switchMap((role: Role) => zip(of(role),
+                this.retrieveChildren(role),
+                this.retrieveParents(role))),
+            map((ra: [Role, Role[], Role[]]) => this.combine(ra))
+        ).subscribe(role => {
+            this.editRole = role;
+            this.originRole = role;
+            this.copyToForm(this.editProperties, this.editRole);
+        }, error => {
+            this.editRole = new Role();
+        });
+    }
+
+    /**
+     * Array of [role, children[], parents[]]
+     */
+    combine(roleArray: [Role, Role[], Role[]]): Role {
+        roleArray[0].children = roleArray[1];
+        roleArray[0].parents = roleArray[2];
+        return roleArray[0];
+    }
+
+    private createRole(id: string): Role {
+        let role = new Role();
+        role.id = id;
+        role.name=''
+        return role;
+    }
+
+    getCachedRole(id : string) : Observable<Role> {
+        return of(id).pipe(
+            switchMap(( myId : string )  => {
+                if (this.roleCache.has(myId)) {
+                    return of(this.roleCache.get(myId));
+                } else {
+                    return this.roleService.getRole(myId).pipe(tap(role => {
+                        this.roleCache.set(role.id, role);
+                    }),catchError(() => of(this.createRole(id))));
+                }
+            }));
+    }
+
+    retrieveChildren(role: Role): Observable<Role[]> {
+        // ForkJoin does not emit, if one of the observables is failing to emit a object
+        // -> we use catchError()
+        let children: Array<Observable<Role>> = []
+        for (let child_id of role.child_role_ids) {
+            children.push(this.getCachedRole(child_id));
+        }
+        if (children.length>0) {
+            return forkJoin(children);
+        } else {
+            return of([]);
+        }
+    }
+
+    retrieveParents(role: Role): Observable<Role[]> {
+        let parents: Array<Observable<Role>> = []
+        for (let parent_id of role.parent_role_ids) {
+            parents.push(this.getCachedRole(parent_id));
+        }
+        if (parents.length>0) {
+            return forkJoin(parents);
+        } else {
+            return of([]);
+        }
+    }
+
+    onSubmit() {
+        let role = this.copyFromForm(this.editProperties);
+        this.roleService.updateRole(role).pipe(
+            catchError((err: ErrorResult) => {
+                this.error = true;
+                this.success = false;
+                this.errorResult = err;
+                return [];
+            })
+        ).subscribe(roleInfo => {
+            this.error = false;
+            this.success = true;
+            this.errorResult = null;
+            this.result = roleInfo;
+            this.editMode = false;
+        });
+
+    }
 
 }
+
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/roles/manage-roles-list/manage-roles-list.component.html b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/roles/manage-roles-list/manage-roles-list.component.html
index 8b21e6c..7d904e7 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/roles/manage-roles-list/manage-roles-list.component.html
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/roles/manage-roles-list/manage-roles-list.component.html
@@ -26,6 +26,7 @@
             <app-th-sorted [fieldArray]="['id']" contentText="roles.attributes.id"></app-th-sorted>
             <app-th-sorted [fieldArray]="['name']" contentText="roles.attributes.name" ></app-th-sorted>
             <app-th-sorted [fieldArray]="['description']" contentText="roles.attributes.description" ></app-th-sorted>
+            <app-th-sorted [fieldArray]="['assignable']" contentText="roles.attributes.assignable"></app-th-sorted>
             <app-th-sorted [fieldArray]="['template_instance']" contentText="roles.attributes.template_instance" ></app-th-sorted>
             <app-th-sorted [fieldArray]="['resource']" contentText="roles.attributes.resource" ></app-th-sorted>
             <th>Action</th>
@@ -36,6 +37,8 @@
             <td><span data-toggle="tooltip" placement="left" ngbTooltip="{{role.id}}">{{role.id}}</span></td>
             <td>{{role.name}}</td>
             <td>{{role.description}}</td>
+            <td><span class="far" [attr.aria-valuetext]="role.assignable"
+                      [ngClass]="role.assignable?'fa-check-circle':'fa-circle'"></span></td>
             <td><span class="far" [attr.aria-valuetext]="role.template_instance"
                       [ngClass]="role.template_instance?'fa-check-circle':'fa-circle'"></span></td>
             <td>{{role.resource}}</td>
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/roles/manage-roles-list/manage-roles-list.component.ts b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/roles/manage-roles-list/manage-roles-list.component.ts
index e3998d7..b49f64d 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/roles/manage-roles-list/manage-roles-list.component.ts
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/roles/manage-roles-list/manage-roles-list.component.ts
@@ -25,21 +25,20 @@ import {Observable} from "rxjs";
 import {PagedResult} from "@app/model/paged-result";
 import {UserInfo} from "@app/model/user-info";
 import {RoleService} from "@app/services/role.service";
+import {SortedTableComponent} from "@app/modules/shared/sorted-table-component";
 
 @Component({
   selector: 'app-manage-roles-list',
   templateUrl: './manage-roles-list.component.html',
   styleUrls: ['./manage-roles-list.component.scss']
 })
-export class ManageRolesListComponent implements OnInit {
+export class ManageRolesListComponent extends SortedTableComponent<Role> implements OnInit {
 
-  service: EntityService<Role>
-
-  constructor(private translator: TranslateService, private roleService : RoleService) {
-    this.service = function (searchTerm: string, offset: number, limit: number, orderBy: string[], order: string) : Observable<PagedResult<Role>> {
+  constructor(translator: TranslateService, roleService : RoleService) {
+    super(translator, function (searchTerm: string, offset: number, limit: number, orderBy: string[], order: string): Observable<PagedResult<Role>> {
       console.log("Retrieving data " + searchTerm + "," + offset + "," + limit + "," + orderBy + "," + order);
       return roleService.query(searchTerm, offset, limit, orderBy, order);
-    }
+    });
   }
 
   ngOnInit(): void {
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/roles/manage-roles/manage-roles.component.ts b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/roles/manage-roles/manage-roles.component.ts
index f2a67dd..74af9c2 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/roles/manage-roles/manage-roles.component.ts
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/roles/manage-roles/manage-roles.component.ts
@@ -39,7 +39,6 @@ export class ManageRolesComponent implements OnInit {
     // console.log("Activating "+componentReference+" - "+JSON.stringify(componentReference,getCircularReplacer()))
     if (componentReference.roleIdEvent!=null) {
       let componentEmit : Observable<string> = componentReference.roleIdEvent.pipe(
-          tap(userid=>console.log("Event "+componentReference.class+" "+userid)),
           map((userid: string) => this.getSubPath(userid)));
       if (this.roleId$!=null) {
         this.roleId$ = merge(this.roleId$, componentEmit)
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/users/manage-users-edit/manage-users-edit.component.html b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/users/manage-users-edit/manage-users-edit.component.html
index ba8becf..8f12e3b 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/users/manage-users-edit/manage-users-edit.component.html
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/users/manage-users-edit/manage-users-edit.component.html
@@ -18,7 +18,7 @@
 
 <form class="mt-3 mb-3" [formGroup]="userForm" (ngSubmit)="onSubmit()">
     <div class="form-group row col-md-8" *ngIf="!editUser.permanent">
-        <div class="col-md-1">Edit <span class="fas fa-edit"></span></div>
+        <div class="col-md-1">{{'form.edit' |translate}} <span class="fas fa-edit"></span></div>
         <div class="col-md-6">
             <input class="form-check-input" type="checkbox" [value]="editMode" [checked]="editMode"
                    (change)="editMode=!editMode"
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/edit-base.component.ts b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/edit-base.component.ts
new file mode 100644
index 0000000..29a1d0f
--- /dev/null
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/edit-base.component.ts
@@ -0,0 +1,133 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import {AbstractControl, FormBuilder, FormControl, FormGroup, ValidationErrors, ValidatorFn} from "@angular/forms";
+import {ErrorResult} from '@app/model/error-result';
+
+export abstract class EditBaseComponent<T> {
+
+    editProperties = ['id'];
+    success: boolean = false;
+    error: boolean = false;
+    errorResult: ErrorResult;
+    result: T;
+    formInitialValues;
+    public userForm : FormGroup;
+    public editMode: boolean;
+
+    constructor(public fb: FormBuilder) {
+
+    }
+
+    init(userForm: FormGroup) : void {
+        this.userForm=userForm;
+        this.formInitialValues = userForm.value;
+    }
+
+    abstract createEntity() : T;
+    abstract onSubmit();
+
+    public copyFromForm(properties: string[]): T {
+        let entity: any = this.createEntity();
+        for (let prop of properties) {
+            entity[prop] = this.userForm.get(prop).value;
+        }
+        return entity;
+    }
+
+    public copyToForm(properties: string[], user: T): void {
+        let propMap = {};
+        for (let prop of properties) {
+            let propValue = user[prop] == null ? '' : user[prop];
+            propMap[prop] = propValue;
+        }
+        this.userForm.patchValue(propMap);
+    }
+
+
+    valid(field: string): string[] {
+        if (this.editMode) {
+            let classArr = this.isValid(field);
+            return classArr.concat('form-control')
+        } else {
+            return ['form-control-plaintext'];
+        }
+    }
+
+    isValid(field: string): string[] {
+        let formField = this.userForm.get(field);
+        if (formField.dirty || formField.touched) {
+            if (formField.valid) {
+                return ['is-valid']
+            } else {
+                return ['is-invalid']
+            }
+        } else {
+            return ['']
+        }
+    }
+
+    forbiddenNameValidator(nameRe: RegExp): ValidatorFn {
+        return (control: AbstractControl): { [key: string]: any } | null => {
+            const forbidden = nameRe.test(control.value);
+            return forbidden ? {forbiddenName: {value: control.value}} : null;
+        };
+    }
+
+    getAllErrors(formGroup: FormGroup, errors: string[] = []) : string[] {
+        Object.keys(formGroup.controls).forEach(field => {
+            const control = formGroup.get(field);
+            if (control instanceof FormControl && control.errors != null) {
+                let keys = Object.keys(control.errors).map(errorKey=>field+'.'+errorKey);
+                errors = errors.concat(keys);
+            } else if (control instanceof FormGroup) {
+                errors = errors.concat(this.getAllErrors(control));
+            }
+        });
+        return errors;
+    }
+
+    getAttributeErrors(control:string):string[] {
+        return Object.keys(this.userForm.get(control).errors);
+    }
+}
+
+export function whitespaceValidator(): ValidatorFn {
+    return (control: AbstractControl): ValidationErrors | null => {
+        const hasWhitespace =  /\s/g.test(control.value);
+        return hasWhitespace ? {containsWhitespace: {value: control.value}} : null;
+    };
+}
+export function MustMatch(controlName: string, matchingControlName: string) : ValidatorFn  {
+    return (formGroup: FormGroup): ValidationErrors | null => {
+        const control = formGroup.controls[controlName];
+        const matchingControl = formGroup.controls[matchingControlName];
+
+        if (matchingControl.errors && !matchingControl.errors.mustMatch) {
+            // return if another validator has already found an error on the matchingControl
+            return;
+        }
+
+        // set error on matchingControl if validation fails
+        if (control.value !== matchingControl.value) {
+            matchingControl.setErrors({mustMatch: true});
+        } else {
+            matchingControl.setErrors(null);
+        }
+    }
+}
\ No newline at end of file
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/shared.module.ts b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/shared.module.ts
index 1030390..27978a3 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/shared.module.ts
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/shared.module.ts
@@ -21,12 +21,13 @@ import {CommonModule} from '@angular/common';
 import {PaginatedEntitiesComponent} from "./paginated-entities/paginated-entities.component";
 import {SortedTableHeaderComponent} from "./sorted-table-header/sorted-table-header.component";
 import {SortedTableHeaderRowComponent} from "./sorted-table-header-row/sorted-table-header-row.component";
-import {NgbPaginationModule, NgbTooltipModule} from "@ng-bootstrap/ng-bootstrap";
+import {NgbAccordionModule, NgbPaginationModule, NgbTooltipModule} from "@ng-bootstrap/ng-bootstrap";
 import {TranslateCompiler, TranslateLoader, TranslateModule} from "@ngx-translate/core";
 import {TranslateMessageFormatCompiler} from "ngx-translate-messageformat-compiler";
 import {HttpClient} from "@angular/common/http";
 import {TranslateHttpLoader} from "@ngx-translate/http-loader";
 import {RouterModule} from "@angular/router";
+import {SortedTableComponent} from "@app/modules/shared/sorted-table-component";
 
 
 @NgModule({
@@ -41,6 +42,7 @@ import {RouterModule} from "@angular/router";
         TranslateModule,
         NgbPaginationModule,
         NgbTooltipModule,
+        NgbAccordionModule,
         PaginatedEntitiesComponent,
         SortedTableHeaderComponent,
         SortedTableHeaderRowComponent
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/sorted-table-component.ts b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/sorted-table-component.ts
new file mode 100644
index 0000000..27423fa
--- /dev/null
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/sorted-table-component.ts
@@ -0,0 +1,11 @@
+import {EntityService} from "@app/model/entity-service";
+import {TranslateService} from "@ngx-translate/core";
+
+export class SortedTableComponent<T> {
+
+    sortField = ["id"];
+    sortOrder = "asc";
+
+    constructor(public translator : TranslateService, public service: EntityService<T>) {
+    }
+}
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/services/role.service.ts b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/services/role.service.ts
index 79bee94..3de9919 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/services/role.service.ts
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/services/role.service.ts
@@ -45,7 +45,6 @@ export class RoleService {
   }
 
   public query(searchTerm: string, offset: number = 0, limit: number = 10, orderBy: string[] = ['id'], order: string = 'asc'): Observable<PagedResult<Role>> {
-    console.log("getRoleList " + searchTerm + "," + offset + "," + limit + "," + orderBy + "," + order);
     if (searchTerm == null) {
       searchTerm = ""
     }
@@ -61,4 +60,12 @@ export class RoleService {
     });
   }
 
+  public getRole(roleId:string) : Observable<Role> {
+    return this.rest.executeRestCall("get", "redback", "roles/" + roleId, null);
+  }
+
+  public updateRole(role:Role) : Observable<Role> {
+    return this.rest.executeRestCall("put", "redback", "roles/" + role.id, role);
+  }
+
 }
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/assets/i18n/en.json b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/assets/i18n/en.json
index 6c39c16..7bdf9b8 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/assets/i18n/en.json
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/assets/i18n/en.json
@@ -132,16 +132,29 @@
       "head": "Roles List"
     },
     "edit": {
-      "head": "Edit/View Role"
+      "head": "Edit/View Role",
+      "parents": "Parents",
+      "children": "Children",
+      "permissions": "Permissions",
+      "users": "Users"
     },
     "attributes": {
       "id": "Identifier",
       "name": "Name",
       "description": "Description",
-      "template_instance": "Template Instance"
+      "template_instance": "Template Instance",
+      "resource": "Repository",
+      "assignable": "Assignable"
     }
   },
 
+  "permissions": {
+    "attributes": {
+      "permission": "Permission",
+      "operation": "Operation",
+      "resource": "Resource"
+    }
+  },
   "search": {
     "button": "Search",
     "label": "Enter your search term",
@@ -158,7 +171,8 @@
       "yes": "Yes",
       "no": "No",
       "save": "Save Changes"
-    }
+    },
+    "edit": "Edit"
   },
   "password": {
     "violations" : {


[archiva] 02/04: Adding loading spinner

Posted by ma...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

martin_s pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/archiva.git

commit b988a30f2c3f9b718953b70e2130337ec4ffe6d2
Author: Martin Stockhammer <ma...@apache.org>
AuthorDate: Sat Dec 19 23:54:23 2020 +0100

    Adding loading spinner
---
 .../home.component.scss => model/role-update.ts}   |  12 ++-
 .../src/main/archiva-web/src/app/model/role.ts     |   5 +
 .../manage-roles-edit.component.html               |  23 +++--
 .../manage-roles-edit.component.ts                 |  22 ++--
 .../manage-roles-list.component.html               |  74 ++++++++------
 .../manage-roles-list.component.ts                 |  14 ++-
 .../manage-users-list.component.html               | 112 +++++++++++++--------
 .../manage-users-list.component.ts                 |  12 +--
 .../app/modules/shared/about/about.component.html  |   5 +-
 .../app/modules/shared/about/about.component.scss  |   5 +-
 .../modules/shared/about/about.component.spec.ts   |   3 +-
 .../app/modules/shared/about/about.component.ts    |   3 +-
 .../modules/shared/contact/contact.component.html  |   5 +-
 .../modules/shared/contact/contact.component.scss  |   5 +-
 .../shared/contact/contact.component.spec.ts       |   3 +-
 .../modules/shared/contact/contact.component.ts    |   3 +-
 .../app/modules/shared/home/home.component.html    |   5 +-
 .../app/modules/shared/home/home.component.scss    |   5 +-
 .../app/modules/shared/home/home.component.spec.ts |   3 +-
 .../src/app/modules/shared/home/home.component.ts  |   3 +-
 .../app/modules/shared/login/login.component.html  |   5 +-
 .../app/modules/shared/login/login.component.scss  |   5 +-
 .../modules/shared/login/login.component.spec.ts   |   3 +-
 .../app/modules/shared/login/login.component.ts    |   3 +-
 .../loading-value.spec.ts}                         |  11 +-
 .../src/app/modules/shared/model/loading-value.ts  |  62 ++++++++++++
 .../page-query.spec.ts}                            |  11 +-
 .../home.component.scss => model/page-query.ts}    |   9 +-
 .../shared/not-found/not-found.component.html      |   5 +-
 .../shared/not-found/not-found.component.scss      |   5 +-
 .../shared/not-found/not-found.component.spec.ts   |   3 +-
 .../shared/not-found/not-found.component.ts        |   3 +-
 .../paginated-entities.component.ts                |  41 ++++++--
 .../src/app/modules/shared/shared.module.ts        |  16 ++-
 .../shared/sidemenu/sidemenu.component.html        |   5 +-
 .../shared/sidemenu/sidemenu.component.scss        |   5 +-
 .../shared/sidemenu/sidemenu.component.spec.ts     |   3 +-
 .../modules/shared/sidemenu/sidemenu.component.ts  |   3 +-
 .../app/modules/shared/sorted-table-component.ts   |  18 ++++
 ...e.component.scss => strip-loading.pipe.spec.ts} |  12 ++-
 .../shared/strip-loading.pipe.ts}                  |  40 +++-----
 ...me.component.scss => with-loading.pipe.spec.ts} |  12 ++-
 .../shared/with-loading.pipe.ts}                   |  49 +++++----
 .../src/app/services/archiva-request.service.ts    |   4 +
 .../archiva-web/src/app/services/role.service.ts   |   5 +-
 .../src/main/archiva-web/src/assets/i18n/en.json   |   3 +
 46 files changed, 424 insertions(+), 239 deletions(-)

diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/home/home.component.scss b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/model/role-update.ts
similarity index 76%
copy from archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/home/home.component.scss
copy to archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/model/role-update.ts
index 042f3ce..4fff84e 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/home/home.component.scss
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/model/role-update.ts
@@ -7,8 +7,7 @@
  * "License"); you may not use this file except in compliance
  * with the License.  You may obtain a copy of the License at
  *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
+ * http://www.apache.org/licenses/LICENSE-2.0
  * Unless required by applicable law or agreed to in writing,
  * software distributed under the License is distributed on an
  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
@@ -16,3 +15,12 @@
  * specific language governing permissions and limitations
  * under the License.
  */
+
+export class RoleUpdate {
+    id: string;
+    description: string;
+
+    public toString = () : string => {
+        return 'RoleUpdate: id='+this.id+', description='+this.description;
+    }
+}
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/model/role.ts b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/model/role.ts
index ac0b469..d2b77af 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/model/role.ts
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/model/role.ts
@@ -43,4 +43,9 @@ export class Role {
     root_path: Array<string>
     assigned_origin: boolean;
 
+
+    public toString = () : string => {
+        return 'Role: id='+this.id+', name='+this.name+', description='+this.description;
+    }
+
 }
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/roles/manage-roles-edit/manage-roles-edit.component.html b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/roles/manage-roles-edit/manage-roles-edit.component.html
index d92d21c..b146c72 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/roles/manage-roles-edit/manage-roles-edit.component.html
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/roles/manage-roles-edit/manage-roles-edit.component.html
@@ -18,9 +18,9 @@
 
 <form class="mt-3 mb-3" [formGroup]="userForm" (ngSubmit)="onSubmit()">
     <div class="form-group row col-md-8">
-        <div class="col-md-1" *ngIf="editRole && !editRole.permanent">{{'form.edit' |translate}}&nbsp;<span
+        <div class="col-md-1" >{{'form.edit' |translate}}&nbsp;<span
                 class="fas fa-edit"></span></div>
-        <div class="col-md-6" *ngIf="editRole && !editRole.permanent">
+        <div class="col-md-6" >
             <input class="form-check-input" type="checkbox" [value]="editMode" [checked]="editMode"
                    (change)="editMode=!editMode"
             >
@@ -29,8 +29,7 @@
     <div class="form-group row col-md-8">
         <label class="col-md-2 col-form-label" for="id">{{'roles.attributes.id' |translate}}</label>
         <div class="col-md-6">
-            <input type="text" formControlName="id" id="id"
-                   [ngClass]="valid('id')"
+            <input type="text" formControlName="id" id="id" class="form-control-plaintext"
                    [attr.readonly]="true">
         </div>
     </div>
@@ -38,8 +37,8 @@
         <label class="col-md-2 col-form-label" for="id">{{'roles.attributes.name' |translate}}</label>
         <div class="col-md-6">
             <input type="text" formControlName="name" id="name"
-                   [ngClass]="valid('name')"
-                   [attr.readonly]="editMode?null:'true'">
+                   class="form-control-plaintext"
+                   [attr.readonly]="true">
         </div>
     </div>
     <div class="form-group row col-md-8">
@@ -53,14 +52,13 @@
     <div class="form-group row col-md-8">
         <label class="col-md-2 col-form-label" for="id">{{'roles.attributes.resource' |translate}}</label>
         <div class="col-md-6">
-            <input type="text" formControlName="resource" id="resource"
-                   [ngClass]="valid('resource')"
+            <input type="text" formControlName="resource" id="resource" class="form-control-plaintext"
                    [attr.readonly]="true">
         </div>
     </div>
     <div class="form-group row col-md-8">
         <div class="col-md-2"></div>
-        <div class="col-md-6">
+        <div class="col-md-4">
             <div class="form-check">
                 <input class="form-check-input" type="checkbox" formControlName="assignable"
                        id="assignable" [attr.disabled]="true">
@@ -76,6 +74,10 @@
                 </label>
             </div>
         </div>
+        <div class="col-md-2" *ngIf="editMode">
+           <button class="btn btn-primary" type="submit"
+            [disabled]="userForm.invalid || !userForm.dirty">{{'form.button.save'|translate}}</button>
+        </div>
     </div>
 </form>
 <hr/>
@@ -83,7 +85,8 @@
     <ngb-panel id="parents" >
         <ng-template ngbPanelHeader let-opened="opened">
             <div class="d-flex align-items-center justify-content-between">
-                <button ngbPanelToggle class="btn btn-link text-left shadow-none"><h3>{{'roles.edit.parents'|translate}}</h3></button>
+                <button ngbPanelToggle class="btn btn-link text-left shadow-none">
+                    <h3>{{'roles.edit.parents'|translate}}</h3></button>
                 <ng-container *ngIf="!opened"><i class="fa fa-eye-slash"></i></ng-container>
                 <ng-container *ngIf="opened"><i class="fa fa-eye"></i></ng-container>
             </div>
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/roles/manage-roles-edit/manage-roles-edit.component.ts b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/roles/manage-roles-edit/manage-roles-edit.component.ts
index cbbd5d5..b5d033f 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/roles/manage-roles-edit/manage-roles-edit.component.ts
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/roles/manage-roles-edit/manage-roles-edit.component.ts
@@ -16,7 +16,7 @@
  * under the License.
  */
 
-import {Component, EventEmitter, OnInit, Output} from '@angular/core';
+import {AfterContentInit, Component, EventEmitter, OnInit, Output} from '@angular/core';
 import {ActivatedRoute} from "@angular/router";
 import {FormBuilder, Validators} from "@angular/forms";
 import {RoleService} from "@app/services/role.service";
@@ -25,15 +25,14 @@ import {Role} from '@app/model/role';
 import {ErrorResult} from "@app/model/error-result";
 import {EditBaseComponent} from "@app/modules/shared/edit-base.component";
 import {forkJoin, iif, Observable, of, pipe, zip} from 'rxjs';
+import {RoleUpdate} from "@app/model/role-update";
 
 @Component({
     selector: 'app-manage-roles-edit',
     templateUrl: './manage-roles-edit.component.html',
     styleUrls: ['./manage-roles-edit.component.scss']
 })
-export class ManageRolesEditComponent extends EditBaseComponent<Role> implements OnInit {
-
-    parentsOpened: boolean
+export class ManageRolesEditComponent extends EditBaseComponent<Role> implements OnInit, AfterContentInit {
 
     editRole: Role;
     editProperties = ['id', 'name', 'description', 'template_instance', 'resource', 'assignable'];
@@ -47,7 +46,7 @@ export class ManageRolesEditComponent extends EditBaseComponent<Role> implements
     constructor(private route: ActivatedRoute, private roleService: RoleService, public fb: FormBuilder) {
         super(fb);
         super.init(fb.group({
-            id: ['', [Validators.required]],
+            id: [''],
             name: ['', Validators.required],
             description: [''],
             resource: [''],
@@ -81,6 +80,8 @@ export class ManageRolesEditComponent extends EditBaseComponent<Role> implements
         });
     }
 
+
+
     /**
      * Array of [role, children[], parents[]]
      */
@@ -137,7 +138,10 @@ export class ManageRolesEditComponent extends EditBaseComponent<Role> implements
     }
 
     onSubmit() {
-        let role = this.copyFromForm(this.editProperties);
+        let role = new RoleUpdate();
+        role.id=this.userForm.get('id').value;
+        role.description = this.userForm.get('description').value;
+        console.log("Submitting changes " + role);
         this.roleService.updateRole(role).pipe(
             catchError((err: ErrorResult) => {
                 this.error = true;
@@ -155,5 +159,11 @@ export class ManageRolesEditComponent extends EditBaseComponent<Role> implements
 
     }
 
+    ngAfterContentInit(): void {
+        if (this.originRole) {
+            this.editRole = this.originRole;
+        }
+    }
+
 }
 
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/roles/manage-roles-list/manage-roles-list.component.html b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/roles/manage-roles-list/manage-roles-list.component.html
index 7d904e7..01654af 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/roles/manage-roles-list/manage-roles-list.component.html
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/roles/manage-roles-list/manage-roles-list.component.html
@@ -18,37 +18,51 @@
 
 <app-paginated-entities [service]="service" pageSize="10" [(sortField)]="sortField" [(sortOrder)]="sortOrder"
                         #parent>
+    <ng-container *ngIf="parent.items$ |async as roleItemLoader" >
+        <ng-template [ngIf]="roleItemLoader.loading" #spinner let-modal>
+            <div class="fixed-top d-flex justify-content-center mt-5 pt-5">
+            <div class="spinner-border text-info mt-5" role="status">
+                <span class="sr-only">Loading...</span>
+            </div>
+            </div>
+        </ng-template>
+    </ng-container>
+    <ng-container *ngIf="parent.items$ |stripLoading|async as roleItem" >
+            <table class="table table-striped table-bordered">
+                <thead class="thead-light">
+                <tr sorted [sortFieldEmitter]="parent.sortFieldChange" [sortOrderEmitter]="parent.sortOrderChange"
+                    [toggleObserver]="parent">
+                    <app-th-sorted [fieldArray]="['id']" contentText="roles.attributes.id"></app-th-sorted>
+                    <app-th-sorted [fieldArray]="['name']" contentText="roles.attributes.name"></app-th-sorted>
+                    <app-th-sorted [fieldArray]="['description']"
+                                   contentText="roles.attributes.description"></app-th-sorted>
+                    <app-th-sorted [fieldArray]="['assignable']"
+                                   contentText="roles.attributes.assignable"></app-th-sorted>
+                    <app-th-sorted [fieldArray]="['template_instance']"
+                                   contentText="roles.attributes.template_instance"></app-th-sorted>
+                    <app-th-sorted [fieldArray]="['resource']" contentText="roles.attributes.resource"></app-th-sorted>
+                    <th>{{'headers.action' |translate}}</th>
+                </tr>
+                </thead>
+                <tbody>
+                <tr *ngFor="let role  of roleItem.data" [ngClass]="(role.permanent)?'table-secondary':''">
+                    <td><span data-toggle="tooltip" placement="left" ngbTooltip="{{role.id}}">{{role.id}}</span></td>
+                    <td>{{role.name}}</td>
+                    <td>{{role.description}}</td>
+                    <td><span class="far" [attr.aria-valuetext]="role.assignable"
+                              [ngClass]="role.assignable?'fa-check-circle':'fa-circle'"></span></td>
+                    <td><span class="far" [attr.aria-valuetext]="role.template_instance"
+                              [ngClass]="role.template_instance?'fa-check-circle':'fa-circle'"></span></td>
+                    <td>{{role.resource}}</td>
+                    <td>
+                        <a [routerLink]="['..','edit', role.id]" [queryParams]="{editmode:true}"
+                           [attr.title]="'roles.edit.head' |translate"><span class="fas fa-edit"></span></a>
 
-    <table class="table table-striped table-bordered">
-        <thead class="thead-light">
-        <tr sorted [sortFieldEmitter]="parent.sortFieldChange" [sortOrderEmitter]="parent.sortOrderChange"
-            [toggleObserver]="parent">
-            <app-th-sorted [fieldArray]="['id']" contentText="roles.attributes.id"></app-th-sorted>
-            <app-th-sorted [fieldArray]="['name']" contentText="roles.attributes.name" ></app-th-sorted>
-            <app-th-sorted [fieldArray]="['description']" contentText="roles.attributes.description" ></app-th-sorted>
-            <app-th-sorted [fieldArray]="['assignable']" contentText="roles.attributes.assignable"></app-th-sorted>
-            <app-th-sorted [fieldArray]="['template_instance']" contentText="roles.attributes.template_instance" ></app-th-sorted>
-            <app-th-sorted [fieldArray]="['resource']" contentText="roles.attributes.resource" ></app-th-sorted>
-            <th>Action</th>
-        </tr>
-        </thead>
-        <tbody>
-        <tr *ngFor="let role  of parent.items$ | async" [ngClass]="(role.permanent)?'table-secondary':''">
-            <td><span data-toggle="tooltip" placement="left" ngbTooltip="{{role.id}}">{{role.id}}</span></td>
-            <td>{{role.name}}</td>
-            <td>{{role.description}}</td>
-            <td><span class="far" [attr.aria-valuetext]="role.assignable"
-                      [ngClass]="role.assignable?'fa-check-circle':'fa-circle'"></span></td>
-            <td><span class="far" [attr.aria-valuetext]="role.template_instance"
-                      [ngClass]="role.template_instance?'fa-check-circle':'fa-circle'"></span></td>
-            <td>{{role.resource}}</td>
-            <td>
-                <a  [routerLink]="['..','edit', role.id]" [queryParams]="{editmode:true}" [attr.title]="'roles.edit.head' |translate"><span class="fas fa-edit"></span></a>
-
-            </td>
-        </tr>
-        </tbody>
-    </table>
+                    </td>
+                </tr>
+                </tbody>
+            </table>
+    </ng-container>
 
 </app-paginated-entities>
 
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/roles/manage-roles-list/manage-roles-list.component.ts b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/roles/manage-roles-list/manage-roles-list.component.ts
index b49f64d..1344b11 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/roles/manage-roles-list/manage-roles-list.component.ts
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/roles/manage-roles-list/manage-roles-list.component.ts
@@ -16,16 +16,15 @@
  * under the License.
  */
 
-import {Component, OnInit} from '@angular/core';
+import {Component, OnInit, TemplateRef, ViewChild} from '@angular/core';
 import {TranslateService} from "@ngx-translate/core";
-import {UserService} from "@app/services/user.service";
-import {EntityService} from "@app/model/entity-service";
 import {Role} from "@app/model/role";
 import {Observable} from "rxjs";
 import {PagedResult} from "@app/model/paged-result";
-import {UserInfo} from "@app/model/user-info";
 import {RoleService} from "@app/services/role.service";
 import {SortedTableComponent} from "@app/modules/shared/sorted-table-component";
+import {delay} from "rxjs/operators";
+import {NgbModal} from "@ng-bootstrap/ng-bootstrap";
 
 @Component({
   selector: 'app-manage-roles-list',
@@ -34,7 +33,10 @@ import {SortedTableComponent} from "@app/modules/shared/sorted-table-component";
 })
 export class ManageRolesListComponent extends SortedTableComponent<Role> implements OnInit {
 
-  constructor(translator: TranslateService, roleService : RoleService) {
+  @ViewChild('content') public spinnerTemplate: TemplateRef<any>;
+
+
+  constructor(translator: TranslateService, roleService : RoleService, private ngbModal:NgbModal) {
     super(translator, function (searchTerm: string, offset: number, limit: number, orderBy: string[], order: string): Observable<PagedResult<Role>> {
       console.log("Retrieving data " + searchTerm + "," + offset + "," + limit + "," + orderBy + "," + order);
       return roleService.query(searchTerm, offset, limit, orderBy, order);
@@ -45,4 +47,6 @@ export class ManageRolesListComponent extends SortedTableComponent<Role> impleme
   }
 
 
+
+
 }
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/users/manage-users-list/manage-users-list.component.html b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/users/manage-users-list/manage-users-list.component.html
index 0731487..d87fc57 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/users/manage-users-list/manage-users-list.component.html
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/users/manage-users-list/manage-users-list.component.html
@@ -19,50 +19,76 @@
 <app-paginated-entities [service]="service" pageSize="10" [(sortField)]="sortField" [(sortOrder)]="sortOrder"
                         #parent>
 
-    <table class="table table-striped table-bordered">
-        <thead class="thead-light">
-        <tr sorted [sortFieldEmitter]="parent.sortFieldChange" [sortOrderEmitter]="parent.sortOrderChange"
-            [toggleObserver]="parent">
-            <app-th-sorted [fieldArray]="['user_id']" contentText="users.attributes.user_id"></app-th-sorted>
-            <app-th-sorted [fieldArray]="['full_name']" contentText="users.attributes.full_name" ></app-th-sorted>
-            <app-th-sorted [fieldArray]="['email']" contentText="users.attributes.email" ></app-th-sorted>
-            <app-th-sorted [fieldArray]="['validated','user_id']">
+    <ng-container *ngIf="parent.items$ |async as roleItemLoader">
+        <ng-template [ngIf]="roleItemLoader.loading">
+            <div class="spinner-border text-primary" role="status">
+                <span class="sr-only">Loading...</span>
+            </div>
+        </ng-template>
+    </ng-container>
+    <ng-container *ngIf="parent.items$ |stripLoading|async as roleItem">
+
+            <table class="table table-striped table-bordered">
+                <thead class="thead-light">
+                <tr sorted [sortFieldEmitter]="parent.sortFieldChange" [sortOrderEmitter]="parent.sortOrderChange"
+                    [toggleObserver]="parent">
+                    <app-th-sorted [fieldArray]="['user_id']" contentText="users.attributes.user_id"></app-th-sorted>
+                    <app-th-sorted [fieldArray]="['full_name']"
+                                   contentText="users.attributes.full_name"></app-th-sorted>
+                    <app-th-sorted [fieldArray]="['email']" contentText="users.attributes.email"></app-th-sorted>
+                    <app-th-sorted [fieldArray]="['validated','user_id']">
             <span class="fas fa-check" placement="top"
                   [ngbTooltip]="heads.validated" [attr.aria-label]="heads.validated"></span>
-            </app-th-sorted>
-            <app-th-sorted [fieldArray]="['locked','user_id']"><span class="fas fa-lock" placement="top"
-                                  [ngbTooltip]="heads.locked" [attr.aria-label]="heads.locked"></span></app-th-sorted>
-            <app-th-sorted [fieldArray]="['password_change_required','user_id']"><span class="fa fa-chevron-circle-right" placement="top"
-                                  [ngbTooltip]="heads.password_change_required" [attr.aria-label]="heads.password_change_required"></span>
-            </app-th-sorted>
-            <app-th-sorted [fieldArray]="['last_login']" contentText="users.attributes.last_login"></app-th-sorted>
-            <app-th-sorted [fieldArray]="['created']" contentText="users.attributes.created" ></app-th-sorted>
-            <app-th-sorted [fieldArray]="['last_password_change']" contentText="users.attributes.last_password_change"></app-th-sorted>
-            <th>Action</th>
-        </tr>
-        </thead>
-        <tbody>
-        <tr *ngFor="let user  of parent.items$ | async" [ngClass]="(user.permanent||user.readOnly)?'table-secondary':''">
-            <td><span data-toggle="tooltip" placement="left" ngbTooltip="{{user.id}}">{{user.user_id}}</span></td>
-            <td>{{user.full_name}}</td>
-            <td>{{user.email}}</td>
-            <td><span class="far" [attr.aria-valuetext]="user.validated"
-                      [ngClass]="user.validated?'fa-check-circle':'fa-circle'"></span></td>
-            <td><span class="far" [attr.aria-valuetext]="user.locked"
-                      [ngClass]="user.locked?'fa-check-circle':'fa-circle'"></span></td>
-            <td><span class="far" [attr.aria-valuetext]="user.password_change_required"
-                      [ngClass]="user.password_change_required?'fa-check-circle':'fa-circle'"></span></td>
-            <td>{{user.timestamp_last_login | date:'yyyy-MM-ddTHH:mm:ss'}}</td>
-            <td>{{user.timestamp_account_creation | date : 'yyyy-MM-ddTHH:mm:ss'}}</td>
-            <td>{{user.timestamp_last_password_change| date : 'yyyy-MM-ddTHH:mm:ss'}}</td>
-            <td><ng-container *ngIf="!user.permanent">
-                <a  [routerLink]="['..','edit', user.user_id]" [queryParams]="{editmode:true}" [attr.title]="'users.edit.head' |translate"><span class="fas fa-edit"></span></a>
-                &nbsp;&nbsp;<a [routerLink]="['..','delete',user.user_id]" [attr.title]="'users.delete.head'|translate"><span class="fas fa-user-minus"></span></a>
-                &nbsp;&nbsp;<a [routerLink]="['..','roles',user.user_id]" [attr.title]="'users.roles.head'|translate"><span class="fas fa-user-tag" ></span></a>
-            </ng-container>
-            </td>
-        </tr>
-        </tbody>
-    </table>
+                    </app-th-sorted>
+                    <app-th-sorted [fieldArray]="['locked','user_id']"><span class="fas fa-lock" placement="top"
+                                                                             [ngbTooltip]="heads.locked"
+                                                                             [attr.aria-label]="heads.locked"></span>
+                    </app-th-sorted>
+                    <app-th-sorted [fieldArray]="['password_change_required','user_id']"><span
+                            class="fa fa-chevron-circle-right" placement="top"
+                            [ngbTooltip]="heads.password_change_required"
+                            [attr.aria-label]="heads.password_change_required"></span>
+                    </app-th-sorted>
+                    <app-th-sorted [fieldArray]="['last_login']"
+                                   contentText="users.attributes.last_login"></app-th-sorted>
+                    <app-th-sorted [fieldArray]="['created']" contentText="users.attributes.created"></app-th-sorted>
+                    <app-th-sorted [fieldArray]="['last_password_change']"
+                                   contentText="users.attributes.last_password_change"></app-th-sorted>
+                    <th>Action</th>
+                </tr>
+                </thead>
+                <tbody>
+                <tr *ngFor="let user  of roleItem.data"
+                    [ngClass]="(user.permanent||user.readOnly)?'table-secondary':''">
+                    <td><span data-toggle="tooltip" placement="left" ngbTooltip="{{user.id}}">{{user.user_id}}</span>
+                    </td>
+                    <td>{{user.full_name}}</td>
+                    <td>{{user.email}}</td>
+                    <td><span class="far" [attr.aria-valuetext]="user.validated"
+                              [ngClass]="user.validated?'fa-check-circle':'fa-circle'"></span></td>
+                    <td><span class="far" [attr.aria-valuetext]="user.locked"
+                              [ngClass]="user.locked?'fa-check-circle':'fa-circle'"></span></td>
+                    <td><span class="far" [attr.aria-valuetext]="user.password_change_required"
+                              [ngClass]="user.password_change_required?'fa-check-circle':'fa-circle'"></span></td>
+                    <td>{{user.timestamp_last_login | date:'yyyy-MM-ddTHH:mm:ss'}}</td>
+                    <td>{{user.timestamp_account_creation | date : 'yyyy-MM-ddTHH:mm:ss'}}</td>
+                    <td>{{user.timestamp_last_password_change| date : 'yyyy-MM-ddTHH:mm:ss'}}</td>
+                    <td>
+                        <ng-container *ngIf="!user.permanent">
+                            <a [routerLink]="['..','edit', user.user_id]" [queryParams]="{editmode:true}"
+                               [attr.title]="'users.edit.head' |translate"><span class="fas fa-edit"></span></a>
+                            &nbsp;&nbsp;<a [routerLink]="['..','delete',user.user_id]"
+                                           [attr.title]="'users.delete.head'|translate"><span
+                                class="fas fa-user-minus"></span></a>
+                            &nbsp;&nbsp;<a [routerLink]="['..','roles',user.user_id]"
+                                           [attr.title]="'users.roles.head'|translate"><span
+                                class="fas fa-user-tag"></span></a>
+                        </ng-container>
+                    </td>
+                </tr>
+                </tbody>
+            </table>
+    </ng-container>
+
 
 </app-paginated-entities>
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/users/manage-users-list/manage-users-list.component.ts b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/users/manage-users-list/manage-users-list.component.ts
index 36b3627..b22aeea 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/users/manage-users-list/manage-users-list.component.ts
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/users/manage-users-list/manage-users-list.component.ts
@@ -16,13 +16,13 @@
  * under the License.
  */
 
-import {Component, OnInit, Input, OnDestroy} from '@angular/core';
+import {Component, Input, OnInit} from '@angular/core';
 import {TranslateService} from "@ngx-translate/core";
-import {UserService} from "../../../../services/user.service";
-import {UserInfo} from "../../../../model/user-info";
-import {EntityService} from "../../../../model/entity-service";
-import {Observable, of} from "rxjs";
-import {PagedResult} from "../../../../model/paged-result";
+import {UserInfo} from "@app/model/user-info";
+import {EntityService} from "@app/model/entity-service";
+import {Observable} from "rxjs";
+import {PagedResult} from "@app/model/paged-result";
+import {UserService} from '@app/services/user.service';
 
 @Component({
   selector: 'app-manage-users-list',
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/about/about.component.html b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/about/about.component.html
index 2c8e6de..806221f 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/about/about.component.html
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/about/about.component.html
@@ -7,13 +7,12 @@
   ~ "License"); you may not use this file except in compliance
   ~ with the License.  You may obtain a copy of the License at
   ~
-  ~   http://www.apache.org/licenses/LICENSE-2.0
-  ~
+  ~ http://www.apache.org/licenses/LICENSE-2.0
   ~ Unless required by applicable law or agreed to in writing,
   ~ software distributed under the License is distributed on an
   ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
   ~ KIND, either express or implied.  See the License for the
   ~ specific language governing permissions and limitations
   ~ under the License.
--->
+  -->
 <p>Apache Archiva Web Console</p>
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/about/about.component.scss b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/about/about.component.scss
index 042f3ce..a0ef995 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/about/about.component.scss
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/about/about.component.scss
@@ -1,4 +1,4 @@
-/*
+/*!
  * Licensed to the Apache Software Foundation (ASF) under one
  * or more contributor license agreements.  See the NOTICE file
  * distributed with this work for additional information
@@ -7,8 +7,7 @@
  * "License"); you may not use this file except in compliance
  * with the License.  You may obtain a copy of the License at
  *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
+ * http://www.apache.org/licenses/LICENSE-2.0
  * Unless required by applicable law or agreed to in writing,
  * software distributed under the License is distributed on an
  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/about/about.component.spec.ts b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/about/about.component.spec.ts
index ddeaebb..7c45c53 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/about/about.component.spec.ts
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/about/about.component.spec.ts
@@ -7,8 +7,7 @@
  * "License"); you may not use this file except in compliance
  * with the License.  You may obtain a copy of the License at
  *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
+ * http://www.apache.org/licenses/LICENSE-2.0
  * Unless required by applicable law or agreed to in writing,
  * software distributed under the License is distributed on an
  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/about/about.component.ts b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/about/about.component.ts
index 1e95c0b..eb1b353 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/about/about.component.ts
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/about/about.component.ts
@@ -7,8 +7,7 @@
  * "License"); you may not use this file except in compliance
  * with the License.  You may obtain a copy of the License at
  *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
+ * http://www.apache.org/licenses/LICENSE-2.0
  * Unless required by applicable law or agreed to in writing,
  * software distributed under the License is distributed on an
  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/contact/contact.component.html b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/contact/contact.component.html
index 8ea7322..d52079a 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/contact/contact.component.html
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/contact/contact.component.html
@@ -7,13 +7,12 @@
   ~ "License"); you may not use this file except in compliance
   ~ with the License.  You may obtain a copy of the License at
   ~
-  ~   http://www.apache.org/licenses/LICENSE-2.0
-  ~
+  ~ http://www.apache.org/licenses/LICENSE-2.0
   ~ Unless required by applicable law or agreed to in writing,
   ~ software distributed under the License is distributed on an
   ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
   ~ KIND, either express or implied.  See the License for the
   ~ specific language governing permissions and limitations
   ~ under the License.
--->
+  -->
 <p><a href="mailto:users@archiva.apache.org">User Mailing List</a></p>
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/contact/contact.component.scss b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/contact/contact.component.scss
index 042f3ce..a0ef995 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/contact/contact.component.scss
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/contact/contact.component.scss
@@ -1,4 +1,4 @@
-/*
+/*!
  * Licensed to the Apache Software Foundation (ASF) under one
  * or more contributor license agreements.  See the NOTICE file
  * distributed with this work for additional information
@@ -7,8 +7,7 @@
  * "License"); you may not use this file except in compliance
  * with the License.  You may obtain a copy of the License at
  *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
+ * http://www.apache.org/licenses/LICENSE-2.0
  * Unless required by applicable law or agreed to in writing,
  * software distributed under the License is distributed on an
  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/contact/contact.component.spec.ts b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/contact/contact.component.spec.ts
index f475933..7f7014f 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/contact/contact.component.spec.ts
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/contact/contact.component.spec.ts
@@ -7,8 +7,7 @@
  * "License"); you may not use this file except in compliance
  * with the License.  You may obtain a copy of the License at
  *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
+ * http://www.apache.org/licenses/LICENSE-2.0
  * Unless required by applicable law or agreed to in writing,
  * software distributed under the License is distributed on an
  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/contact/contact.component.ts b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/contact/contact.component.ts
index 7435690..b2d03f6 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/contact/contact.component.ts
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/contact/contact.component.ts
@@ -7,8 +7,7 @@
  * "License"); you may not use this file except in compliance
  * with the License.  You may obtain a copy of the License at
  *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
+ * http://www.apache.org/licenses/LICENSE-2.0
  * Unless required by applicable law or agreed to in writing,
  * software distributed under the License is distributed on an
  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/home/home.component.html b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/home/home.component.html
index dfac255..bea4271 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/home/home.component.html
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/home/home.component.html
@@ -7,15 +7,14 @@
   ~ "License"); you may not use this file except in compliance
   ~ with the License.  You may obtain a copy of the License at
   ~
-  ~   http://www.apache.org/licenses/LICENSE-2.0
-  ~
+  ~ http://www.apache.org/licenses/LICENSE-2.0
   ~ Unless required by applicable law or agreed to in writing,
   ~ software distributed under the License is distributed on an
   ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
   ~ KIND, either express or implied.  See the License for the
   ~ specific language governing permissions and limitations
   ~ under the License.
--->
+  -->
 <div class="row">
   <div class="col-2">
     <app-sidemenu></app-sidemenu>
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/home/home.component.scss b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/home/home.component.scss
index 042f3ce..a0ef995 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/home/home.component.scss
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/home/home.component.scss
@@ -1,4 +1,4 @@
-/*
+/*!
  * Licensed to the Apache Software Foundation (ASF) under one
  * or more contributor license agreements.  See the NOTICE file
  * distributed with this work for additional information
@@ -7,8 +7,7 @@
  * "License"); you may not use this file except in compliance
  * with the License.  You may obtain a copy of the License at
  *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
+ * http://www.apache.org/licenses/LICENSE-2.0
  * Unless required by applicable law or agreed to in writing,
  * software distributed under the License is distributed on an
  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/home/home.component.spec.ts b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/home/home.component.spec.ts
index f7c82e5..2f7e651 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/home/home.component.spec.ts
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/home/home.component.spec.ts
@@ -7,8 +7,7 @@
  * "License"); you may not use this file except in compliance
  * with the License.  You may obtain a copy of the License at
  *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
+ * http://www.apache.org/licenses/LICENSE-2.0
  * Unless required by applicable law or agreed to in writing,
  * software distributed under the License is distributed on an
  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/home/home.component.ts b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/home/home.component.ts
index c5b55f3..2f44afe 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/home/home.component.ts
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/home/home.component.ts
@@ -7,8 +7,7 @@
  * "License"); you may not use this file except in compliance
  * with the License.  You may obtain a copy of the License at
  *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
+ * http://www.apache.org/licenses/LICENSE-2.0
  * Unless required by applicable law or agreed to in writing,
  * software distributed under the License is distributed on an
  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/login/login.component.html b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/login/login.component.html
index 04fad9f..c30dc73 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/login/login.component.html
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/login/login.component.html
@@ -7,15 +7,14 @@
   ~ "License"); you may not use this file except in compliance
   ~ with the License.  You may obtain a copy of the License at
   ~
-  ~   http://www.apache.org/licenses/LICENSE-2.0
-  ~
+  ~ http://www.apache.org/licenses/LICENSE-2.0
   ~ Unless required by applicable law or agreed to in writing,
   ~ software distributed under the License is distributed on an
   ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
   ~ KIND, either express or implied.  See the License for the
   ~ specific language governing permissions and limitations
   ~ under the License.
--->
+  -->
 
 <!-- Modal -->
 <div class="modal fade" id="loginModal" tabindex="-1" role="dialog" aria-labelledby="loginModal" aria-hidden="true">
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/login/login.component.scss b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/login/login.component.scss
index 7220975..16e476a 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/login/login.component.scss
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/login/login.component.scss
@@ -1,4 +1,4 @@
-/*
+/*!
  * Licensed to the Apache Software Foundation (ASF) under one
  * or more contributor license agreements.  See the NOTICE file
  * distributed with this work for additional information
@@ -7,8 +7,7 @@
  * "License"); you may not use this file except in compliance
  * with the License.  You may obtain a copy of the License at
  *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
+ * http://www.apache.org/licenses/LICENSE-2.0
  * Unless required by applicable law or agreed to in writing,
  * software distributed under the License is distributed on an
  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/login/login.component.spec.ts b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/login/login.component.spec.ts
index 307e95b..2f92cf4 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/login/login.component.spec.ts
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/login/login.component.spec.ts
@@ -7,8 +7,7 @@
  * "License"); you may not use this file except in compliance
  * with the License.  You may obtain a copy of the License at
  *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
+ * http://www.apache.org/licenses/LICENSE-2.0
  * Unless required by applicable law or agreed to in writing,
  * software distributed under the License is distributed on an
  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/login/login.component.ts b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/login/login.component.ts
index 0a12e1a..b67a97f 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/login/login.component.ts
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/login/login.component.ts
@@ -7,8 +7,7 @@
  * "License"); you may not use this file except in compliance
  * with the License.  You may obtain a copy of the License at
  *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
+ * http://www.apache.org/licenses/LICENSE-2.0
  * Unless required by applicable law or agreed to in writing,
  * software distributed under the License is distributed on an
  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/home/home.component.scss b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/model/loading-value.spec.ts
similarity index 77%
copy from archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/home/home.component.scss
copy to archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/model/loading-value.spec.ts
index 042f3ce..0ad6490 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/home/home.component.scss
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/model/loading-value.spec.ts
@@ -7,8 +7,7 @@
  * "License"); you may not use this file except in compliance
  * with the License.  You may obtain a copy of the License at
  *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
+ * http://www.apache.org/licenses/LICENSE-2.0
  * Unless required by applicable law or agreed to in writing,
  * software distributed under the License is distributed on an
  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
@@ -16,3 +15,11 @@
  * specific language governing permissions and limitations
  * under the License.
  */
+
+import { LoadingValue } from './loading-value';
+
+describe('LoadingValue', () => {
+  it('should create an instance', () => {
+    expect(new LoadingValue()).toBeTruthy();
+  });
+});
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/model/loading-value.ts b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/model/loading-value.ts
new file mode 100644
index 0000000..c66818e
--- /dev/null
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/model/loading-value.ts
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+export class LoadingValue<T> {
+    loading: boolean;
+    value: T;
+    error: any;
+
+    public hasError() : boolean {
+        return this.error != null;
+    }
+
+    public hasValue() : boolean {
+        return this.value != null;
+    }
+
+    static start<T>() : LoadingValue<T> {
+        let lv = new LoadingValue<T>();
+        lv.loading=true;
+        return lv;
+    }
+
+    static finish<T>(value: T) : LoadingValue<T> {
+        let lv = new LoadingValue<T>();
+        lv.loading=false;
+        lv.value = value;
+        return lv;
+    }
+
+    static of<T>(type: string, value: T = null) : LoadingValue<T>{
+        let lv = new LoadingValue<T>();
+        if (type=='start') {
+            lv.loading=true;
+        } else if (type=='finish') {
+            lv.loading=false;
+            lv.value=value;
+        }
+        return lv;
+    }
+
+    static error<T>(error:any) : LoadingValue<T> {
+        let lv = new LoadingValue<T>();
+        lv.loading=false;
+        lv.error=error;
+        return lv;
+    }
+}
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/home/home.component.scss b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/model/page-query.spec.ts
similarity index 77%
copy from archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/home/home.component.scss
copy to archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/model/page-query.spec.ts
index 042f3ce..13b03a1 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/home/home.component.scss
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/model/page-query.spec.ts
@@ -7,8 +7,7 @@
  * "License"); you may not use this file except in compliance
  * with the License.  You may obtain a copy of the License at
  *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
+ * http://www.apache.org/licenses/LICENSE-2.0
  * Unless required by applicable law or agreed to in writing,
  * software distributed under the License is distributed on an
  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
@@ -16,3 +15,11 @@
  * specific language governing permissions and limitations
  * under the License.
  */
+
+import { PageQuery } from './page-query';
+
+describe('PageQuery', () => {
+  it('should create an instance', () => {
+    expect(new PageQuery()).toBeTruthy();
+  });
+});
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/home/home.component.scss b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/model/page-query.ts
similarity index 84%
copy from archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/home/home.component.scss
copy to archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/model/page-query.ts
index 042f3ce..00b543c 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/home/home.component.scss
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/model/page-query.ts
@@ -7,8 +7,7 @@
  * "License"); you may not use this file except in compliance
  * with the License.  You may obtain a copy of the License at
  *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
+ * http://www.apache.org/licenses/LICENSE-2.0
  * Unless required by applicable law or agreed to in writing,
  * software distributed under the License is distributed on an
  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
@@ -16,3 +15,9 @@
  * specific language governing permissions and limitations
  * under the License.
  */
+
+export class PageQuery {
+
+    constructor(public search: string, public page: number) {
+    }
+}
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/not-found/not-found.component.html b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/not-found/not-found.component.html
index cb9cb9f..27b5098 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/not-found/not-found.component.html
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/not-found/not-found.component.html
@@ -7,13 +7,12 @@
   ~ "License"); you may not use this file except in compliance
   ~ with the License.  You may obtain a copy of the License at
   ~
-  ~   http://www.apache.org/licenses/LICENSE-2.0
-  ~
+  ~ http://www.apache.org/licenses/LICENSE-2.0
   ~ Unless required by applicable law or agreed to in writing,
   ~ software distributed under the License is distributed on an
   ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
   ~ KIND, either express or implied.  See the License for the
   ~ specific language governing permissions and limitations
   ~ under the License.
--->
+  -->
 <p>URL not found!</p>
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/not-found/not-found.component.scss b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/not-found/not-found.component.scss
index 042f3ce..a0ef995 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/not-found/not-found.component.scss
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/not-found/not-found.component.scss
@@ -1,4 +1,4 @@
-/*
+/*!
  * Licensed to the Apache Software Foundation (ASF) under one
  * or more contributor license agreements.  See the NOTICE file
  * distributed with this work for additional information
@@ -7,8 +7,7 @@
  * "License"); you may not use this file except in compliance
  * with the License.  You may obtain a copy of the License at
  *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
+ * http://www.apache.org/licenses/LICENSE-2.0
  * Unless required by applicable law or agreed to in writing,
  * software distributed under the License is distributed on an
  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/not-found/not-found.component.spec.ts b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/not-found/not-found.component.spec.ts
index 64d1ce0..78ca07b 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/not-found/not-found.component.spec.ts
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/not-found/not-found.component.spec.ts
@@ -7,8 +7,7 @@
  * "License"); you may not use this file except in compliance
  * with the License.  You may obtain a copy of the License at
  *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
+ * http://www.apache.org/licenses/LICENSE-2.0
  * Unless required by applicable law or agreed to in writing,
  * software distributed under the License is distributed on an
  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/not-found/not-found.component.ts b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/not-found/not-found.component.ts
index cef74cc..fefb55e 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/not-found/not-found.component.ts
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/not-found/not-found.component.ts
@@ -7,8 +7,7 @@
  * "License"); you may not use this file except in compliance
  * with the License.  You may obtain a copy of the License at
  *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
+ * http://www.apache.org/licenses/LICENSE-2.0
  * Unless required by applicable law or agreed to in writing,
  * software distributed under the License is distributed on an
  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/paginated-entities/paginated-entities.component.ts b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/paginated-entities/paginated-entities.component.ts
index ef9582f..cc62ac6 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/paginated-entities/paginated-entities.component.ts
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/paginated-entities/paginated-entities.component.ts
@@ -17,10 +17,25 @@
  */
 
 import {AfterViewInit, Component, EventEmitter, Input, OnInit, Output} from '@angular/core';
-import {merge, Observable, Subject} from "rxjs";
-import {debounceTime, distinctUntilChanged, map, mergeMap, pluck, share, startWith} from "rxjs/operators";
+import {concat, merge, Observable, of, pipe, Subject} from "rxjs";
+import {
+    concatAll,
+    debounceTime,
+    delay,
+    distinctUntilChanged,
+    filter,
+    map,
+    mergeMap,
+    pluck,
+    share,
+    startWith,
+    switchMap
+} from "rxjs/operators";
 import {EntityService} from "../../../model/entity-service";
 import {FieldToggle} from "../../../model/field-toggle";
+import {PageQuery} from "@app/modules/shared/model/page-query";
+import { LoadingValue } from '../shared.module';
+import {PagedResult} from "@app/model/paged-result";
 
 
 /**
@@ -107,7 +122,7 @@ export class PaginatedEntitiesComponent<T> implements OnInit, FieldToggle, After
     /**
      * The entity items retrieved from the service
      */
-    items$: Observable<T[]>;
+    items$: Observable<LoadingValue<PagedResult<T>>>;
 
     private pageStream: Subject<number> = new Subject<number>();
     private searchTermStream: Subject<string> = new Subject<string>();
@@ -118,22 +133,26 @@ export class PaginatedEntitiesComponent<T> implements OnInit, FieldToggle, After
     ngOnInit(): void {
         // We combine the sources for the page and the search input field to a observable 'source'
         const pageSource = this.pageStream.pipe(map(pageNumber => {
-            return {search: this.searchTerm, page: pageNumber}
+            return new PageQuery(this.searchTerm, pageNumber);
         }));
         const searchSource = this.searchTermStream.pipe(
             debounceTime(1000),
             distinctUntilChanged(),
             map(searchTerm => {
                 this.searchTerm = searchTerm;
-                return {search: searchTerm, page: 1}
+                return new PageQuery(searchTerm, 1)
             }));
         const source = merge(pageSource, searchSource).pipe(
-            startWith({search: this.searchTerm, page: this.page}),
-            mergeMap((params: { search: string, page: number }) => {
-                return this.service(params.search, (params.page - 1) * this.pageSize, this.pageSize, this.sortField, this.sortOrder);
-            }), share());
-        this.total$ = source.pipe(pluck('pagination', 'total_count'));
-        this.items$ = source.pipe(pluck('data'));
+            startWith(new PageQuery(this.searchTerm, this.page)),
+            switchMap((params: PageQuery) =>
+                concat(
+                    of(LoadingValue.start<PagedResult<T>>()),
+                    this.service(params.search, (params.page - 1) * this.pageSize, this.pageSize, this.sortField, this.sortOrder)
+                        .pipe(map(pagedResult=>LoadingValue.finish<PagedResult<T>>(pagedResult)))
+                )
+            ), share());
+        this.total$ = source.pipe(filter(val=>val.hasValue()),map(val=>val.value),pluck('pagination', 'total_count'));
+        this.items$ = source;
     }
 
     search(terms: string) {
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/shared.module.ts b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/shared.module.ts
index 27978a3..0420576 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/shared.module.ts
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/shared.module.ts
@@ -21,20 +21,25 @@ import {CommonModule} from '@angular/common';
 import {PaginatedEntitiesComponent} from "./paginated-entities/paginated-entities.component";
 import {SortedTableHeaderComponent} from "./sorted-table-header/sorted-table-header.component";
 import {SortedTableHeaderRowComponent} from "./sorted-table-header-row/sorted-table-header-row.component";
-import {NgbAccordionModule, NgbPaginationModule, NgbTooltipModule} from "@ng-bootstrap/ng-bootstrap";
+import {NgbAccordionModule, NgbModalModule, NgbPaginationModule, NgbTooltipModule} from "@ng-bootstrap/ng-bootstrap";
 import {TranslateCompiler, TranslateLoader, TranslateModule} from "@ngx-translate/core";
 import {TranslateMessageFormatCompiler} from "ngx-translate-messageformat-compiler";
 import {HttpClient} from "@angular/common/http";
 import {TranslateHttpLoader} from "@ngx-translate/http-loader";
 import {RouterModule} from "@angular/router";
-import {SortedTableComponent} from "@app/modules/shared/sorted-table-component";
+import { WithLoadingPipe } from './with-loading.pipe';
+import { StripLoadingPipe } from './strip-loading.pipe';
 
+export { LoadingValue } from './model/loading-value';
+export { PageQuery } from './model/page-query';
 
 @NgModule({
     declarations: [
         PaginatedEntitiesComponent,
         SortedTableHeaderComponent,
-        SortedTableHeaderRowComponent
+        SortedTableHeaderRowComponent,
+        WithLoadingPipe,
+        StripLoadingPipe
     ],
     exports: [
         CommonModule,
@@ -43,9 +48,12 @@ import {SortedTableComponent} from "@app/modules/shared/sorted-table-component";
         NgbPaginationModule,
         NgbTooltipModule,
         NgbAccordionModule,
+        NgbModalModule,
         PaginatedEntitiesComponent,
         SortedTableHeaderComponent,
-        SortedTableHeaderRowComponent
+        SortedTableHeaderRowComponent,
+        WithLoadingPipe,
+        StripLoadingPipe
     ],
     imports: [
         CommonModule,
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/sidemenu/sidemenu.component.html b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/sidemenu/sidemenu.component.html
index e147479..cbded1e 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/sidemenu/sidemenu.component.html
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/sidemenu/sidemenu.component.html
@@ -7,15 +7,14 @@
   ~ "License"); you may not use this file except in compliance
   ~ with the License.  You may obtain a copy of the License at
   ~
-  ~   http://www.apache.org/licenses/LICENSE-2.0
-  ~
+  ~ http://www.apache.org/licenses/LICENSE-2.0
   ~ Unless required by applicable law or agreed to in writing,
   ~ software distributed under the License is distributed on an
   ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
   ~ KIND, either express or implied.  See the License for the
   ~ specific language governing permissions and limitations
   ~ under the License.
--->
+  -->
 <nav class="nav flex-column nav-pills " role="tablist" aria-orientation="vertical">
 
     <div class="nav flex-column nav-pills" role="tablist" aria-orientation="vertical"
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/sidemenu/sidemenu.component.scss b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/sidemenu/sidemenu.component.scss
index 042f3ce..a0ef995 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/sidemenu/sidemenu.component.scss
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/sidemenu/sidemenu.component.scss
@@ -1,4 +1,4 @@
-/*
+/*!
  * Licensed to the Apache Software Foundation (ASF) under one
  * or more contributor license agreements.  See the NOTICE file
  * distributed with this work for additional information
@@ -7,8 +7,7 @@
  * "License"); you may not use this file except in compliance
  * with the License.  You may obtain a copy of the License at
  *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
+ * http://www.apache.org/licenses/LICENSE-2.0
  * Unless required by applicable law or agreed to in writing,
  * software distributed under the License is distributed on an
  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/sidemenu/sidemenu.component.spec.ts b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/sidemenu/sidemenu.component.spec.ts
index 7524c81..c74423b 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/sidemenu/sidemenu.component.spec.ts
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/sidemenu/sidemenu.component.spec.ts
@@ -7,8 +7,7 @@
  * "License"); you may not use this file except in compliance
  * with the License.  You may obtain a copy of the License at
  *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
+ * http://www.apache.org/licenses/LICENSE-2.0
  * Unless required by applicable law or agreed to in writing,
  * software distributed under the License is distributed on an
  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/sidemenu/sidemenu.component.ts b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/sidemenu/sidemenu.component.ts
index c4fa16c..ad7b21c 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/sidemenu/sidemenu.component.ts
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/sidemenu/sidemenu.component.ts
@@ -7,8 +7,7 @@
  * "License"); you may not use this file except in compliance
  * with the License.  You may obtain a copy of the License at
  *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
+ * http://www.apache.org/licenses/LICENSE-2.0
  * Unless required by applicable law or agreed to in writing,
  * software distributed under the License is distributed on an
  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/sorted-table-component.ts b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/sorted-table-component.ts
index 27423fa..67d2888 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/sorted-table-component.ts
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/sorted-table-component.ts
@@ -1,3 +1,21 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
 import {EntityService} from "@app/model/entity-service";
 import {TranslateService} from "@ngx-translate/core";
 
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/home/home.component.scss b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/strip-loading.pipe.spec.ts
similarity index 74%
copy from archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/home/home.component.scss
copy to archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/strip-loading.pipe.spec.ts
index 042f3ce..8efe49f 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/home/home.component.scss
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/strip-loading.pipe.spec.ts
@@ -7,8 +7,7 @@
  * "License"); you may not use this file except in compliance
  * with the License.  You may obtain a copy of the License at
  *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
+ * http://www.apache.org/licenses/LICENSE-2.0
  * Unless required by applicable law or agreed to in writing,
  * software distributed under the License is distributed on an
  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
@@ -16,3 +15,12 @@
  * specific language governing permissions and limitations
  * under the License.
  */
+
+import { StripLoadingPipe } from './strip-loading.pipe';
+
+describe('StripLoadingPipe', () => {
+  it('create an instance', () => {
+    const pipe = new StripLoadingPipe();
+    expect(pipe).toBeTruthy();
+  });
+});
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/model/role.ts b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/strip-loading.pipe.ts
similarity index 57%
copy from archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/model/role.ts
copy to archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/strip-loading.pipe.ts
index ac0b469..8a528ae 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/model/role.ts
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/strip-loading.pipe.ts
@@ -16,31 +16,23 @@
  * under the License.
  */
 
-import {Permission} from "@app/model/permission";
+import { Pipe, PipeTransform } from '@angular/core';
+import {isObservable, of} from "rxjs";
+import {catchError, filter, map, startWith, tap} from "rxjs/operators";
+import {LoadingValue} from "@app/modules/shared/model/loading-value";
 
-export class Role {
-    id: string
-    name: string
-    description: string
-    assignable: boolean
-    permanent: boolean
-    child: boolean
-    assigned: boolean
-    template_instance: boolean
-    application_id:string
-    model_id:string
-    resource:string
+@Pipe({
+  name: 'stripLoading'
+})
+export class StripLoadingPipe implements PipeTransform {
 
-    child_role_ids: Array<string>
-    parent_role_ids: Array<string>
-    children: Array<Role>
-    parents: Array<Role>
-    permissions: Array<Permission>
-
-    // Web Internal attributes
-    enabled: boolean = true
-    level:number = -1
-    root_path: Array<string>
-    assigned_origin: boolean;
+  transform(val) {
+    return isObservable(val)
+        ? val.pipe(
+            filter(val => (val instanceof LoadingValue) && val.value),
+            map(( val : LoadingValue<any>) => val.value)
+        )
+        : val;
+  }
 
 }
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/home/home.component.scss b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/with-loading.pipe.spec.ts
similarity index 74%
copy from archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/home/home.component.scss
copy to archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/with-loading.pipe.spec.ts
index 042f3ce..cf08291 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/home/home.component.scss
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/with-loading.pipe.spec.ts
@@ -7,8 +7,7 @@
  * "License"); you may not use this file except in compliance
  * with the License.  You may obtain a copy of the License at
  *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
+ * http://www.apache.org/licenses/LICENSE-2.0
  * Unless required by applicable law or agreed to in writing,
  * software distributed under the License is distributed on an
  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
@@ -16,3 +15,12 @@
  * specific language governing permissions and limitations
  * under the License.
  */
+
+import { WithLoadingPipe } from './with-loading.pipe';
+
+describe('WithLoadingPipe', () => {
+  it('create an instance', () => {
+    const pipe = new WithLoadingPipe();
+    expect(pipe).toBeTruthy();
+  });
+});
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/model/role.ts b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/with-loading.pipe.ts
similarity index 50%
copy from archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/model/role.ts
copy to archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/with-loading.pipe.ts
index ac0b469..8de45ed 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/model/role.ts
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/with-loading.pipe.ts
@@ -16,31 +16,28 @@
  * under the License.
  */
 
-import {Permission} from "@app/model/permission";
-
-export class Role {
-    id: string
-    name: string
-    description: string
-    assignable: boolean
-    permanent: boolean
-    child: boolean
-    assigned: boolean
-    template_instance: boolean
-    application_id:string
-    model_id:string
-    resource:string
-
-    child_role_ids: Array<string>
-    parent_role_ids: Array<string>
-    children: Array<Role>
-    parents: Array<Role>
-    permissions: Array<Permission>
-
-    // Web Internal attributes
-    enabled: boolean = true
-    level:number = -1
-    root_path: Array<string>
-    assigned_origin: boolean;
+import { Pipe, PipeTransform } from '@angular/core';
+import {concat, isObservable, of } from 'rxjs';
+import {catchError, map, startWith, tap } from 'rxjs/operators';
+import {LoadingValue} from "@app/modules/shared/shared.module";
 
+@Pipe({
+  name: 'withLoading'
+})
+export class WithLoadingPipe implements PipeTransform {
+  transform(val) {
+    return isObservable(val)
+        ? val.pipe(
+            map((value: any) => {
+                if(value instanceof LoadingValue) {
+                    return value as LoadingValue<any>;
+                } else {
+                    return LoadingValue.finish(value);
+                }
+            }),
+            startWith(LoadingValue.start()),
+            catchError(error => of(LoadingValue.error(error)))
+        )
+        : val;
+  }
 }
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/services/archiva-request.service.ts b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/services/archiva-request.service.ts
index 84af0d6..2c00a41 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/services/archiva-request.service.ts
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/services/archiva-request.service.ts
@@ -57,6 +57,8 @@ export class ArchivaRequestService {
             return this.http.delete<R>(httpArgs.url, httpArgs.options);
         } else if (lType == "put") {
             return this.http.put<R>(httpArgs.url, input, httpArgs.options);
+        } else if (lType == "patch") {
+            return this.http.patch<R>(httpArgs.url, input, httpArgs.options);
         }
     }
 
@@ -98,6 +100,8 @@ export class ArchivaRequestService {
             return this.http.delete<HttpResponse<R>>(httpArgs.url, httpArgs.options);
         } else if (lType=='put') {
             return this.http.put<HttpResponse<R>>(httpArgs.url, input, httpArgs.options);
+        } else if (lType=='patch') {
+            return this.http.patch<HttpResponse<R>>(httpArgs.url, input, httpArgs.options);
         }
     }
 
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/services/role.service.ts b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/services/role.service.ts
index 3de9919..99424a0 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/services/role.service.ts
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/services/role.service.ts
@@ -24,6 +24,7 @@ import { Role } from '@app/model/role';
 import {HttpResponse} from "@angular/common/http";
 import {PagedResult} from "@app/model/paged-result";
 import {UserInfo} from "@app/model/user-info";
+import {RoleUpdate} from "@app/model/role-update";
 
 @Injectable({
   providedIn: 'root'
@@ -64,8 +65,8 @@ export class RoleService {
     return this.rest.executeRestCall("get", "redback", "roles/" + roleId, null);
   }
 
-  public updateRole(role:Role) : Observable<Role> {
-    return this.rest.executeRestCall("put", "redback", "roles/" + role.id, role);
+  public updateRole(role:RoleUpdate) : Observable<Role> {
+    return this.rest.executeRestCall("patch", "redback", "roles/" + role.id, role);
   }
 
 }
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/assets/i18n/en.json b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/assets/i18n/en.json
index 7bdf9b8..d094e39 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/assets/i18n/en.json
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/assets/i18n/en.json
@@ -174,6 +174,9 @@
     },
     "edit": "Edit"
   },
+  "headers": {
+    "action": "Action"
+  },
   "password": {
     "violations" : {