group management, add group details

This commit is contained in:
Sylvain MATHIEU 2021-12-28 09:27:42 +01:00
parent c3f3fafbef
commit 6122801f90
20 changed files with 557 additions and 47 deletions

View File

@ -57,7 +57,9 @@ import { ServerResolve } from './resolvers/server-resolve';
import { UserManagementComponent } from './components/user-management/user-management.component';
import { LoggedUserComponent } from './components/users/logged-user/logged-user.component';
import {GroupManagementComponent} from "./components/group-management/group-management.component";
import {GroupDetailsComponent} from "@components/group-details/group-details.component";
import {UserDetailComponent} from "@components/user-management/user-detail/user-detail.component";
import {GroupDetailsResolver} from "@resolvers/group-details.resolver";
const routes: Routes = [
{
@ -205,6 +207,8 @@ const routes: Routes = [
{ path: 'server/:server_id/preferences/iou/templates/:template_id/copy', component: CopyIouTemplateComponent, canActivate: [LoginGuard] },
{ path: 'server/:server_id/preferences/iou/addtemplate', component: AddIouTemplateComponent, canActivate: [LoginGuard] },
{ path: 'server/:server_id/group_management', component: GroupManagementComponent},
{ path: 'server/:server_id/group_management/:user_group_id', component: GroupDetailsComponent, resolve: {group: GroupDetailsResolver}},
],
},
{

View File

@ -280,7 +280,10 @@ import { GroupFilterPipe } from './filters/group-filter.pipe';
import { AddGroupDialogComponent } from './components/group-management/add-group-dialog/add-group-dialog.component';
import { DeleteGroupDialogComponent } from './components/group-management/delete-group-dialog/delete-group-dialog.component';
import { DeleteUserDialogComponent } from './components/user-management/delete-user-dialog/delete-user-dialog.component';
import { GroupDetailsComponent } from './components/group-details/group-details.component';
import { UserDetailComponent } from './components/user-management/user-detail/user-detail.component';
import { AddUserToGroupDialogComponent } from './components/group-details/add-user-to-group-dialog/add-user-to-group-dialog.component';
import { RemoveUserToGroupDialogComponent } from './components/group-details/remove-user-to-group-dialog/remove-user-to-group-dialog.component';
@NgModule({
declarations: [
@ -476,7 +479,10 @@ import { UserDetailComponent } from './components/user-management/user-detail/us
UserFilterPipe,
DeleteGroupDialogComponent,
DeleteUserDialogComponent,
UserDetailComponent
GroupDetailsComponent,
UserDetailComponent,
AddUserToGroupDialogComponent,
RemoveUserToGroupDialogComponent
],
imports: [
BrowserModule,

View File

@ -0,0 +1,16 @@
<div class="title">
<h3>Add User To group: {{data.group.name}}</h3>
</div>
<div class="filter">
<mat-form-field class="input-field">
<mat-label>Search user </mat-label>
<input matInput type="text" [(ngModel)]="searchText" (keydown)="onSearch()">
</mat-form-field>
</div>
<div class="userList" *ngFor="let user of displayedUsers | async">
<div>{{user.username}}</div>
<div>{{user.email}}</div>
<mat-icon (click)="addUser(user)" *ngIf="!loading">add</mat-icon>
<mat-spinner *ngIf="loading"></mat-spinner>
</div>

View File

@ -0,0 +1,31 @@
:host {
display: flex;
flex-direction: column;
width: 100%;
}
.title {
width: 100%;
text-align: center;
}
.filter {
display: flex;
width: 600px;
justify-content: center;
margin-bottom: 50px;
}
mat-form-field {
width: 600px;
}
input {
width: 100%;
}
.userList {
display: flex;
justify-content: space-between;
margin-bottom: 10px;
}

View File

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

View File

@ -0,0 +1,79 @@
import {Component, Inject, OnInit} from '@angular/core';
import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog";
import {UserService} from "@services/user.service";
import {Server} from "@models/server";
import {BehaviorSubject, forkJoin, observable, Observable, timer} from "rxjs";
import {User} from "@models/users/user";
import {GroupService} from "@services/group.service";
import {Group} from "@models/groups/group";
import {tap} from "rxjs/operators";
import {ToasterService} from "@services/toaster.service";
@Component({
selector: 'app-add-user-to-group-dialog',
templateUrl: './add-user-to-group-dialog.component.html',
styleUrls: ['./add-user-to-group-dialog.component.scss']
})
export class AddUserToGroupDialogComponent implements OnInit {
users = new BehaviorSubject<User[]>([]);
displayedUsers = new BehaviorSubject<User[]>([]);
searchText: string;
loading = false;
constructor(private dialog: MatDialogRef<AddUserToGroupDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: { server: Server; group: Group },
private userService: UserService,
private groupService: GroupService,
private toastService: ToasterService) {
}
ngOnInit(): void {
this.getUsers();
}
onSearch() {
timer(500)
.subscribe(() => {
const displayedUsers = this.users.value.filter((user: User) => {
return user.username.includes(this.searchText) || user.email?.includes(this.searchText);
});
this.displayedUsers.next(displayedUsers);
});
}
getUsers() {
forkJoin([
this.userService.list(this.data.server),
this.groupService.getGroupMember(this.data.server, this.data.group.user_group_id)
]).subscribe((results) => {
const [userList, members] = results;
const users = userList.filter((user: User) => {
return !members.find((u: User) => u.user_id === user.user_id);
});
this.users.next(users);
this.displayedUsers.next(users);
});
}
addUser(user: User) {
this.loading = true;
this.groupService
.addMemberToGroup(this.data.server, this.data.group, user)
.subscribe(() => {
this.toastService.success(`user ${user.username} was added`);
this.getUsers();
this.loading = false;
}, (err) => {
console.log(err);
this.toastService.error(`error while adding user ${user.username} to group ${this.data.group.name}`);
this.loading = false;
});
}
}

View File

@ -0,0 +1,40 @@
<div class="content">
<div class="default-header">
<div class="row">
<h1 class="col">Groups {{group.name}} details</h1>
</div>
<div class="main">
<div class="details">
<div>
<mat-form-field class="input-field">
<mat-label>Group name:</mat-label>
<input matInput type="text" [ngModel]="group.name">
</mat-form-field>
</div>
<div>Creation date: {{group.created_at}}</div>
<div>Last update Date: {{group.updated_at}}</div>
<div>UUID: {{group.user_group_id}}</div>
<div>
<mat-checkbox [checked]="group.is_builtin" disabled>Is build in</mat-checkbox>
</div>
<div mat-dialog-actions class="button-div">
<button mat-button (click)="onUpdate()" tabindex="2" mat-raised-button color="primary"
[disabled]="!editGroupForm.valid">
Update Group
</button>
</div>
</div>
<div class="members">
<div>
<div class="title"> Members: </div>
<mat-icon (click)="openAddUserDialog()" class="clickable">person_add</mat-icon>
</div>
<div *ngFor="let user of members">
<a href="/server/{{server.id}}/user_management/{{user.user_id}}"><div>{{user.username}}</div></a>
<mat-icon class="clickable" (click)="openRemoveUserDialog(user)">delete</mat-icon>
</div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,46 @@
.main {
display: flex;
justify-content: space-between;
}
.details {
width: 30vw;
display: flex;
flex-direction: column;
justify-content: center;
}
.members {
width: 40vw;
display: flex;
flex-direction: column;
justify-content: stretch;
}
.members > div {
display: flex;
flex-direction: row;
justify-content: space-between;
margin-bottom: 5px;
}
.clickable {
cursor: pointer;
}
.members .title {
font-size: 2em;
text-decoration: underline;
}
.details > div {
margin-bottom: 20px;
}
.input-field {
width: 100%;
}
.button-div {
float: right;
}

View File

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

View File

@ -0,0 +1,98 @@
import {Component, Inject, OnInit} from '@angular/core';
import {ActivatedRoute, Router} from "@angular/router";
import {Server} from "@models/server";
import {Group} from "@models/groups/group";
import {User} from "@models/users/user";
import {FormControl, FormGroup} from "@angular/forms";
import {MatDialog} from "@angular/material/dialog";
import {AddUserToGroupDialogComponent} from "@components/group-details/add-user-to-group-dialog/add-user-to-group-dialog.component";
import {RemoveUserToGroupDialogComponent} from "@components/group-details/remove-user-to-group-dialog/remove-user-to-group-dialog.component";
import {GroupService} from "@services/group.service";
import {ToasterService} from "@services/toaster.service";
@Component({
selector: 'app-group-details',
templateUrl: './group-details.component.html',
styleUrls: ['./group-details.component.scss']
})
export class GroupDetailsComponent implements OnInit {
server: Server;
group: Group;
members: User[];
editGroupForm: FormGroup;
constructor(private route: ActivatedRoute,
private dialog: MatDialog,
private groupService: GroupService,
private toastService: ToasterService) {
this.editGroupForm = new FormGroup({
groupname: new FormControl(''),
});
this.route.data.subscribe((d: { group: { server: Server; group: Group, users: User[] } }) => {
this.server = d.group.server;
this.group = d.group.group;
this.members = d.group.users;
this.editGroupForm.setValue({groupname: this.group.name});
});
}
ngOnInit(): void {
}
onUpdate() {
this.groupService.update(this.server, this.group)
.subscribe(() => {
this.toastService.success(`group updated`);
}, (error) => {
this.toastService.error('Error: Cannot update group');
console.log(error);
});
}
openAddUserDialog() {
this.dialog
.open<AddUserToGroupDialogComponent>(AddUserToGroupDialogComponent,
{
width: '700px', height: '500px',
data: {server: this.server, group: this.group}
})
.afterClosed()
.subscribe(() => {
this.reloadMembers();
});
}
openRemoveUserDialog(user: User) {
this.dialog
.open<RemoveUserToGroupDialogComponent>(RemoveUserToGroupDialogComponent,
{width: '500px', height: '200px', data: {user}})
.afterClosed()
.subscribe((userToRemove: User) => {
console.log(userToRemove);
if (userToRemove) {
this.groupService.removeUser(this.server, this.group, userToRemove)
.subscribe(() => {
this.toastService.success(`User ${user.username} was removed`);
this.reloadMembers();
},
(error) => {
this.toastService.error(`Error while removing user ${user.username} from ${this.group.name}`);
console.log(error);
});
}
});
}
reloadMembers() {
this.groupService.getGroupMember(this.server, this.group.user_group_id)
.subscribe((members: User[]) => {
this.members = members;
});
}
}

View File

@ -0,0 +1,9 @@
<div class="header">
<div>Confirm ?</div>
<div>Removing user: {{data.user.username}}</div>
</div>
<div class="button">
<button mat-button mat-raised-button color="primary" (click)="onCancel()">No, cancel</button>
<button mat-button mat-raised-button color="warn"(click)="onConfirm()">Yes, remove</button>
</div>

View File

@ -0,0 +1,20 @@
:host {
width: 100%;
display: flex;
flex-direction: column;
justify-content: center;
}
.header {
display: flex;
flex-direction: column;
justify-content: center;
text-align: center;
margin-bottom: 20px;
}
.button {
display: flex;
flex-direction: row;
justify-content: space-evenly;
}

View File

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

View File

@ -0,0 +1,25 @@
import {Component, Inject, OnInit} from '@angular/core';
import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog";
import {User} from "@models/users/user";
@Component({
selector: 'app-remove-user-to-group-dialog',
templateUrl: './remove-user-to-group-dialog.component.html',
styleUrls: ['./remove-user-to-group-dialog.component.scss']
})
export class RemoveUserToGroupDialogComponent implements OnInit {
constructor(private dialogRef: MatDialogRef<RemoveUserToGroupDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: { user: User }) { }
ngOnInit(): void {
}
onCancel() {
this.dialogRef.close();
}
onConfirm() {
this.dialogRef.close(this.data.user);
}
}

View File

@ -18,7 +18,7 @@
</form>
<div class="default-content">
<table mat-table [dataSource]="sortedGroups | groupFilter: searchText" class="mat-elevation-z8" matSort (matSortChange)="sort($event)">
<table mat-table [dataSource]="dataSource | groupFilter: searchText" class="mat-elevation-z8" matSort>
<ng-container matColumnDef="select" >
<mat-header-cell *matHeaderCellDef class="small-col">
@ -37,7 +37,7 @@
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Name</th>
<td mat-cell *matCellDef="let element"> {{element.name}} </td>
<td mat-cell *matCellDef="let element"><a href="/server/{{server.id}}/group_management/{{element.user_group_id}}">{{element.name}}</a> </td>
</ng-container>
<ng-container matColumnDef="created_at">
@ -67,5 +67,9 @@
</table>
<mat-paginator [pageSizeOptions]="[5, 10, 20]"
showFirstLastButtons
aria-label="Select page">
</mat-paginator>
</div>
</div>

View File

@ -10,21 +10,21 @@
*
* Author: Sylvain MATHIEU, Elise LEBEAU
*/
import {Component, OnInit} from '@angular/core';
import {ActivatedRoute} from "@angular/router";
import {Component, OnInit, ViewChild} from '@angular/core';
import {ActivatedRoute, Router} from "@angular/router";
import {ServerService} from "../../services/server.service";
import {ToasterService} from "../../services/toaster.service";
import {GroupService} from "../../services/group.service";
import {Server} from "../../models/server";
import {Group} from "../../models/groups/group";
import {Sort} from "@angular/material/sort";
import {MatSort, Sort} from "@angular/material/sort";
import {MatDialog} from "@angular/material/dialog";
import {AddGroupDialogComponent} from "@components/group-management/add-group-dialog/add-group-dialog.component";
import {DeleteGroupDialogComponent} from "@components/group-management/delete-group-dialog/delete-group-dialog.component";
import {SelectionModel} from "@angular/cdk/collections";
import {DeleteUserDialogComponent} from "@components/user-management/delete-user-dialog/delete-user-dialog.component";
import {User} from "@models/users/user";
import {forkJoin} from "rxjs";
import {MatPaginator} from "@angular/material/paginator";
import {MatTableDataSource} from "@angular/material/table";
@Component({
selector: 'app-group-management',
@ -34,10 +34,13 @@ import {forkJoin} from "rxjs";
export class GroupManagementComponent implements OnInit {
server: Server;
@ViewChild(MatPaginator) paginator: MatPaginator;
@ViewChild(MatSort, { static: true }) sort: MatSort;
public displayedColumns = ['select', 'name', 'created_at', 'updated_at', 'is_builtin', 'delete'];
selection = new SelectionModel<Group>(true, []);
groups: Group[];
sortedGroups: Group[];
dataSource = new MatTableDataSource<Group>();
searchText: string;
constructor(
@ -49,44 +52,28 @@ export class GroupManagementComponent implements OnInit {
) {
}
ngOnInit(): void {
const serverId = this.route.snapshot.paramMap.get('server_id');
this.serverService.get(+serverId).then((server: Server) => {
this.server = server;
this.groupService.getGroups(server).subscribe((groups: Group[]) => {
this.groups = groups;
this.sortedGroups = groups;
});
this.refresh();
});
}
sort(sort: Sort) {
const data = this.groups.slice();
if (!sort.active || sort.direction === '') {
this.sortedGroups = data;
return;
}
this.sortedGroups = data.sort((a, b) => {
const isAsc = sort.direction === 'asc';
switch (sort.active) {
case 'name':
return compare(a.name.toLowerCase(), b.name.toLowerCase(), isAsc);
case 'created_at':
return compare(a.created_at, b.created_at, isAsc);
case 'updated_at':
return compare(a.updated_at, b.updated_at, isAsc);
case 'is_builtin':
return compare(a.is_builtin.toString(), b.is_builtin.toString(), isAsc);
ngAfterViewInit() {
this.dataSource.paginator = this.paginator;
this.dataSource.sort = this.sort;
this.dataSource.sortingDataAccessor = (item, property) => {
switch (property) {
case 'username':
case 'full_name':
case 'email':
return item[property] ? item[property].toLowerCase() : '';
default:
return 0;
return item[property];
}
});
function compare(a: number | string, b: number | string, isAsc: boolean) {
return (a < b ? -1 : 1) * (isAsc ? 1 : -1);
}
};
}
isAllSelected() {
@ -120,7 +107,7 @@ export class GroupManagementComponent implements OnInit {
refresh() {
this.groupService.getGroups(this.server).subscribe((groups: Group[]) => {
this.groups = groups;
this.sortedGroups = groups;
this.dataSource.data = groups;
this.selection.clear();
});
}

View File

@ -12,24 +12,22 @@
*/
import {Pipe, PipeTransform} from '@angular/core';
import {Group} from "../models/groups/group";
import {MatTableDataSource} from "@angular/material/table";
@Pipe({
name: 'groupFilter'
})
export class GroupFilterPipe implements PipeTransform {
transform(groups: Group[], searchText: string): Group[] {
if (!groups) {
return [];
}
transform(groups: MatTableDataSource<Group>, searchText: string): MatTableDataSource<Group> {
if (!searchText) {
return groups;
}
searchText = searchText.toLowerCase();
return groups.filter((group: Group) => {
return group.name.toLowerCase().includes(searchText);
});
searchText = searchText.trim().toLowerCase();
groups.filter = searchText;
return groups;
}
}

View File

@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { GroupDetailsResolver } from './group-details.resolver';
describe('GroupDetailsResolver', () => {
let resolver: GroupDetailsResolver;
beforeEach(() => {
TestBed.configureTestingModule({});
resolver = TestBed.inject(GroupDetailsResolver);
});
it('should be created', () => {
expect(resolver).toBeTruthy();
});
});

View File

@ -0,0 +1,40 @@
import { Injectable } from '@angular/core';
import {
Router, Resolve,
RouterStateSnapshot,
ActivatedRouteSnapshot
} from '@angular/router';
import {Observable, of, Subscriber} from 'rxjs';
import {ServerService} from "../services/server.service";
import {GroupService} from "../services/group.service";
import {Server} from "../models/server";
import {Group} from "../models/groups/group";
import {User} from "../models/users/user";
@Injectable({
providedIn: 'root'
})
export class GroupDetailsResolver implements Resolve<{server: Server; group: Group; users: User[]}> {
constructor(private serverService: ServerService,
private groupService: GroupService) {
}
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<{server: Server; group: Group; users: User[]}> {
return new Observable<{server: Server; group: Group; users: User[]}>((subscriber: Subscriber<any>) => {
const serverId = route.paramMap.get('server_id');
const groupId = route.paramMap.get('user_group_id');
this.serverService.get(+serverId).then((server: Server) => {
this.groupService.get(server, groupId).subscribe((group: Group) => {
this.groupService.getGroupMember(server, groupId).subscribe((users: User[]) => {
subscriber.next({server, group, users});
subscriber.complete();
});
});
});
});
}
}

View File

@ -42,4 +42,20 @@ export class GroupService {
delete(server: Server, user_group_id: string) {
return this.httpServer.delete(server, `/groups/${user_group_id}`);
}
get(server: Server, user_group_id: string) {
return this.httpServer.get(server, `/groups/${user_group_id}`);
}
addMemberToGroup(server: Server, group: Group, user: User) {
return this.httpServer.put(server, `/groups/${group.user_group_id}/members/${user.user_id}`, {});
}
removeUser(server: Server, group: Group, user: User) {
return this.httpServer.delete(server, `/groups/${group.user_group_id}/members/${user.user_id}`);
}
update(server: Server, group: Group) {
return this.httpServer.put(server, `/groups/${group.user_group_id}`, {name: group.name});
}
}