feat(console): user memberships, generic member create dialog, fix user autocomplete emitter (#606)

* load manager mgmtservice, user service

* add org memberships

* membership component, generic member creation

* refactor member create dialog

* project autocomplete context

* create batch managers in user component

* project context wrapper

* emit on user removal, preselect user on init

* membership avatar style, service

* auth user memberships, navigate to target

* cursor fix, avatar gen

* lint

* i18n fix

* remove role translations

* membership detail page, i18n

* fix role label i18n, after view init loader

* remove projectid from grant remove

* fix iam race condition

* refresh table ts, fix no permission project search

* change membership colors

* refresh table everywhere, replace assets, routing

* fix logo header size

* lint, fix project grant removal

* timestmp for p mem, user list, grants, p list (#615)

* npm audit

* update deps, resolve vulnerability

* fix tslint config

* update lock

* load 20 changes at once

* Update console/src/assets/i18n/de.json

Co-authored-by: Florian Forster <[email protected]>

* Update console/src/assets/i18n/de.json

Co-authored-by: Florian Forster <[email protected]>

* Update console/src/assets/i18n/en.json

Co-authored-by: Florian Forster <[email protected]>

* membership i18n

Co-authored-by: Florian Forster <[email protected]>
This commit is contained in:
Max Peintner
2020-08-24 08:48:47 +02:00
committed by GitHub
co-authored by Florian Forster
parent d49fee23bf
commit 193cfb45f6
84 changed files with 1972 additions and 2911 deletions
@@ -139,6 +139,8 @@
</div>
</div>
<app-memberships [user]="user"></app-memberships>
<app-changes class="changes" [changeType]="ChangeType.MYUSER" [id]="user.id"></app-changes>
</div>
</app-meta-layout>
@@ -0,0 +1,58 @@
import { DataSource } from '@angular/cdk/collections';
import { Timestamp } from 'google-protobuf/google/protobuf/timestamp_pb';
import { BehaviorSubject, from, Observable, of } from 'rxjs';
import { catchError, finalize, map } from 'rxjs/operators';
import { UserMembershipView } from 'src/app/proto/generated/management_pb';
import { MgmtUserService } from 'src/app/services/mgmt-user.service';
export class MembershipDetailDataSource extends DataSource<UserMembershipView.AsObject> {
public totalResult: number = 0;
public viewTimestamp!: Timestamp.AsObject;
public membersSubject: BehaviorSubject<UserMembershipView.AsObject[]>
= new BehaviorSubject<UserMembershipView.AsObject[]>([]);
private loadingSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
public loading$: Observable<boolean> = this.loadingSubject.asObservable();
constructor(private mgmtUserService: MgmtUserService) {
super();
}
public loadMemberships(userId: string, pageIndex: number, pageSize: number): void {
const offset = pageIndex * pageSize;
this.loadingSubject.next(true);
from(this.mgmtUserService.SearchUserMemberships(userId, pageSize, offset)).pipe(
map(resp => {
const response = resp.toObject();
this.totalResult = response.totalResult;
if (response.viewTimestamp) {
this.viewTimestamp = response.viewTimestamp;
}
return response.resultList;
}),
catchError(() => of([])),
finalize(() => this.loadingSubject.next(false)),
).subscribe(members => {
this.membersSubject.next(members);
});
}
/**
* Connect this data source to the table. The table will only update when
* the returned stream emits new items.
* @returns A stream of the items to be rendered.
*/
public connect(): Observable<UserMembershipView.AsObject[]> {
return this.membersSubject.asObservable();
}
/**
* Called when the table is being destroyed. Use this function, to clean up
* any open connections or free any held resources that were set up during connect.
*/
public disconnect(): void {
this.membersSubject.complete();
this.loadingSubject.complete();
}
}
@@ -0,0 +1,74 @@
<app-detail-layout [backRouterLink]="[ '/users', user?.id]"
title="{{user?.displayName}} {{ 'USER.MEMBERSHIPS.TITLE' | translate }}"
description="{{ 'USER.MEMBERSHIPS.DESCRIPTION' | translate }}">
<app-refresh-table class="refresh-table" (refreshed)="refreshPage()" [dataSize]="dataSource?.totalResult"
[timestamp]="dataSource?.viewTimestamp" [selection]="selection" [loading]="dataSource?.loading$ | async">
<!-- <button actions (click)="removeSelectedMemberships()" matTooltip="{{'USER.MEMBERSHIPS.REMOVE' | translate}}"
class="icon-button" mat-icon-button *ngIf="selection.hasValue()" color="warn">
<i class="las la-trash"></i>
</button> -->
<a actions color="primary" class="add-button" (click)="addMember()" color="primary" mat-raised-button>
<mat-icon class="icon">add</mat-icon>{{ 'ACTIONS.NEW' | translate }}
</a>
<div class="table-wrapper">
<table mat-table class="background-style table" aria-label="Elements" [dataSource]="dataSource">
<ng-container matColumnDef="select">
<th class="selection" mat-header-cell *matHeaderCellDef>
<mat-checkbox color="primary" (change)="$event ? masterToggle() : null"
[checked]="selection.hasValue() && isAllSelected()"
[indeterminate]="selection.hasValue() && !isAllSelected()">
</mat-checkbox>
</th>
<td class="selection" mat-cell *matCellDef="let row">
<mat-checkbox color="primary" (click)="$event.stopPropagation()"
(change)="$event ? selection.toggle(row) : null" [checked]="selection.isSelected(row)">
</mat-checkbox>
</td>
</ng-container>
<ng-container matColumnDef="memberType">
<th mat-header-cell *matHeaderCellDef> {{ 'USER.MEMBERSHIPS.TYPE' | translate }} </th>
<td class="pointer" mat-cell *matCellDef="let member">
{{'USER.MEMBERSHIPS.TYPES.' + member.memberType | translate }} </td>
</ng-container>
<ng-container matColumnDef="displayName">
<th mat-header-cell *matHeaderCellDef> {{ 'USER.MEMBERSHIPS.DISPLAYNAME' | translate }} </th>
<td class="pointer" mat-cell *matCellDef="let member">
{{member.displayName}} </td>
</ng-container>
<ng-container matColumnDef="creationDate">
<th mat-header-cell *matHeaderCellDef> {{ 'USER.MEMBERSHIPS.CREATIONDATE' | translate }} </th>
<td class="pointer" mat-cell *matCellDef="let member">
{{member.creationDate | timestampToDate | localizedDate: 'EEE dd. MMM, HH:mm'}} </td>
</ng-container>
<ng-container matColumnDef="changeDate">
<th mat-header-cell *matHeaderCellDef> {{ 'USER.MEMBERSHIPS.CHANGEDATE' | translate }} </th>
<td class="pointer" mat-cell *matCellDef="let member">
{{member.changeDate | timestampToDate | localizedDate: 'EEE dd. MMM, HH:mm'}}
</td>
</ng-container>
<ng-container matColumnDef="roles">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.MEMBER.ROLES' | translate }} </th>
<td class="pointer" mat-cell *matCellDef="let member">
{{member.rolesList.join(', ')}}
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr class="data-row" mat-row *matRowDef="let row; columns: displayedColumns;">
</tr>
</table>
<mat-paginator class="paginator background-style" #paginator [pageSize]="50"
[pageSizeOptions]="[25, 50, 100, 250]">
</mat-paginator>
</div>
</app-refresh-table>
</app-detail-layout>
@@ -0,0 +1,51 @@
.add-button {
border-radius: .5rem;
}
.refresh-table {
width: 100%;
}
.table-wrapper {
width: 100%;
overflow: auto;
.table,
.paginator {
width: 100%;
td,
th {
padding: .5rem;
&:first-child {
padding-left: 0;
padding-right: 1rem;
}
&:last-child {
padding-right: 0;
}
}
.action {
width: 40px;
}
.data-row {
&:hover {
background-color: #ffffff05;
}
}
.selection {
width: 50px;
max-width: 50px;
}
}
}
.pointer {
outline: none;
cursor: pointer;
}
@@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { MembershipDetailComponent } from './membership-detail.component';
describe('MembershipDetailComponent', () => {
let component: MembershipDetailComponent;
let fixture: ComponentFixture<MembershipDetailComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [MembershipDetailComponent],
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(MembershipDetailComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,213 @@
import { SelectionModel } from '@angular/cdk/collections';
import { AfterViewInit, Component, ViewChild } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { MatPaginator } from '@angular/material/paginator';
import { MatTable } from '@angular/material/table';
import { ActivatedRoute } from '@angular/router';
import { tap } from 'rxjs/operators';
import { CreationType, MemberCreateDialogComponent } from 'src/app/modules/add-member-dialog/member-create-dialog.component';
import { User, UserMembershipSearchResponse, UserMembershipView, UserView } from 'src/app/proto/generated/management_pb';
import { AdminService } from 'src/app/services/admin.service';
import { MgmtUserService } from 'src/app/services/mgmt-user.service';
import { OrgService } from 'src/app/services/org.service';
import { ProjectService } from 'src/app/services/project.service';
import { ToastService } from 'src/app/services/toast.service';
import { MembershipDetailDataSource } from './membership-detail-datasource';
@Component({
selector: 'app-membership-detail',
templateUrl: './membership-detail.component.html',
styleUrls: ['./membership-detail.component.scss'],
})
export class MembershipDetailComponent implements AfterViewInit {
public user!: UserView.AsObject;
@ViewChild(MatPaginator) public paginator!: MatPaginator;
@ViewChild(MatTable) public table!: MatTable<UserMembershipView.AsObject>;
public dataSource!: MembershipDetailDataSource;
public selection: SelectionModel<UserMembershipView.AsObject>
= new SelectionModel<UserMembershipView.AsObject>(true, []);
public memberRoleOptions: string[] = [];
/** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */
public displayedColumns: string[] = ['select', 'memberType', 'displayName', 'creationDate', 'changeDate', 'roles'];
public loading: boolean = false;
public memberships!: UserMembershipSearchResponse.AsObject;
constructor(
private mgmtUserService: MgmtUserService,
activatedRoute: ActivatedRoute,
private dialog: MatDialog,
private toast: ToastService,
private projectService: ProjectService,
private orgService: OrgService,
private adminService: AdminService,
) {
activatedRoute.params.subscribe(data => {
const { id } = data;
if (id) {
this.mgmtUserService.GetUserByID(id).then(user => {
this.user = user.toObject();
this.dataSource = new MembershipDetailDataSource(this.mgmtUserService);
this.dataSource.loadMemberships(
this.user.id,
0,
50,
);
}).catch(err => {
console.error(err);
});
}
});
}
public ngAfterViewInit(): void {
this.paginator.page
.pipe(
tap(() => this.loadMembershipsPage()),
)
.subscribe();
}
private loadMembershipsPage(): void {
this.dataSource.loadMemberships(
this.user.id,
this.paginator.pageIndex,
this.paginator.pageSize,
);
}
// public removeSelectedMemberships(): void {
// Promise.all(this.selection.selected.map(membership => {
// switch (membership.memberType) {
// case MemberType.MEMBERTYPE_ORGANISATION:
// return this.orgService.RemoveMyOrgMember(membership.objectId);
// case MemberType.MEMBERTYPE_PROJECT:
// return this.projectService.RemoveProjectMember(membership.objectId, this.user.id);
// // case MemberType.MEMBERTYPE_PROJECT_GRANT:
// // return this.projectService.RemoveProjectGrantMember(membership.objectId, this.user.id);
// }
// }));
// }
public isAllSelected(): boolean {
const numSelected = this.selection.selected.length;
const numRows = this.dataSource.membersSubject.value.length;
return numSelected === numRows;
}
public masterToggle(): void {
this.isAllSelected() ?
this.selection.clear() :
this.dataSource.membersSubject.value.forEach(row => this.selection.select(row));
}
public addMember(): void {
const dialogRef = this.dialog.open(MemberCreateDialogComponent, {
width: '400px',
data: {
user: this.user,
},
});
dialogRef.afterClosed().subscribe(resp => {
if (resp && resp.creationType !== undefined) {
switch (resp.creationType) {
case CreationType.IAM:
this.createIamMember(resp);
break;
case CreationType.ORG:
this.createOrgMember(resp);
break;
case CreationType.PROJECT_OWNED:
this.createOwnedProjectMember(resp);
break;
case CreationType.PROJECT_GRANTED:
this.createGrantedProjectMember(resp);
break;
}
}
});
}
public async loadManager(userId: string): Promise<void> {
this.mgmtUserService.SearchUserMemberships(userId, 100, 0, []).then(response => {
this.memberships = response.toObject();
this.loading = false;
});
}
public createIamMember(response: any): void {
const users: User.AsObject[] = response.users;
const roles: string[] = response.roles;
if (users && users.length && roles && roles.length) {
Promise.all(users.map(user => {
return this.adminService.AddIamMember(user.id, roles);
})).then(() => {
this.toast.showInfo('IAM.TOAST.MEMBERADDED', true);
}).catch(error => {
this.toast.showError(error);
});
}
}
private createOrgMember(response: any): void {
const users: User.AsObject[] = response.users;
const roles: string[] = response.roles;
if (users && users.length && roles && roles.length) {
Promise.all(users.map(user => {
return this.orgService.AddMyOrgMember(user.id, roles);
})).then(() => {
this.toast.showInfo('ORG.TOAST.MEMBERADDED', true);
}).catch(error => {
this.toast.showError(error);
});
}
}
private createGrantedProjectMember(response: any): void {
const users: User.AsObject[] = response.users;
const roles: string[] = response.roles;
if (users && users.length && roles && roles.length) {
users.forEach(user => {
return this.projectService.AddProjectGrantMember(
response.projectId,
response.grantId,
user.id,
roles,
).then(() => {
this.toast.showInfo('PROJECT.TOAST.MEMBERADDED', true);
}).catch(error => {
this.toast.showError(error);
});
});
}
}
private createOwnedProjectMember(response: any): void {
const users: User.AsObject[] = response.users;
const roles: string[] = response.roles;
if (users && users.length && roles && roles.length) {
users.forEach(user => {
return this.projectService.AddProjectMember(response.projectId, user.id, roles)
.then(() => {
this.toast.showInfo('PROJECT.TOAST.MEMBERADDED', true);
}).catch(error => {
this.toast.showError(error);
});
});
}
}
public refreshPage(): void {
this.selection.clear();
this.dataSource.loadMemberships(this.user.id, this.paginator.pageIndex, this.paginator.pageSize);
}
}
@@ -0,0 +1,50 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatIconModule } from '@angular/material/icon';
import { MatPaginatorModule } from '@angular/material/paginator';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatTableModule } from '@angular/material/table';
import { MatTooltipModule } from '@angular/material/tooltip';
import { RouterModule, Routes } from '@angular/router';
import { TranslateModule } from '@ngx-translate/core';
import { HasRoleModule } from 'src/app/directives/has-role/has-role.module';
import { DetailLayoutModule } from 'src/app/modules/detail-layout/detail-layout.module';
import { RefreshTableModule } from 'src/app/modules/refresh-table/refresh-table.module';
import { LocalizedDatePipeModule } from 'src/app/pipes/localized-date-pipe.module';
import { TimestampToDatePipeModule } from 'src/app/pipes/timestamp-to-date-pipe.module';
import { MembershipDetailComponent } from './membership-detail.component';
const routes: Routes = [
{
path: '',
component: MembershipDetailComponent,
canActivate: [],
data: {
roles: ['user.write'],
},
},
];
@NgModule({
declarations: [MembershipDetailComponent],
imports: [
CommonModule,
RouterModule.forChild(routes),
TranslateModule,
DetailLayoutModule,
MatCheckboxModule,
MatTableModule,
MatPaginatorModule,
MatProgressSpinnerModule,
LocalizedDatePipeModule,
TimestampToDatePipeModule,
HasRoleModule,
MatIconModule,
MatButtonModule,
RefreshTableModule,
MatTooltipModule,
],
})
export class MembershipDetailModule { }
@@ -0,0 +1,37 @@
<div class="membership-groups">
<span class="header">{{ 'USER.MEMBERSHIPS.TITLE' | translate }}</span>
<!-- <span class="sub-header">{{ 'USER,' }}</span> -->
<div class="people" *ngIf="memberships">
<div class="img-list" [@cardAnimation]="memberships.totalResult">
<mat-spinner class="spinner" diameter="20" *ngIf="loading"></mat-spinner>
<ng-container *ngIf="memberships.totalResult < 10; else compact">
<ng-container *ngFor="let membership of memberships.resultList; index as i">
<div @animate class="avatar-circle" (click)="navigateToObject()"
matTooltip="{{ membership.displayName }} | {{membership.rolesList?.join(' ')}}"
[ngStyle]="{'z-index': 100 - i}">
<div class="membership-avatar"
[ngStyle]="{'background-color': getColor(membership.memberType)}">
<i *ngIf="membership.memberType == MemberType.MEMBERTYPE_ORGANISATION"
class="las la-archway"></i>
<i *ngIf="membership.memberType == MemberType.MEMBERTYPE_PROJECT"
class="icon las la-layer-group"></i>
<i *ngIf="membership.memberType == MemberType.MEMBERTYPE_PROJECT_GRANT"
class="icon las la-layer-group"></i>
<span>{{membership.displayName}}</span>
</div>
</div>
</ng-container>
</ng-container>
<ng-template #compact>
<div class="avatar-circle" matTooltip="Click to show detail">
<span>{{memberships.totalResult}}</span>
</div>
</ng-template>
<button class="add-img" (click)="addMember()" mat-icon-button aria-label="add membership">
<mat-icon>add</mat-icon>
</button>
</div>
</div>
</div>
@@ -0,0 +1,109 @@
@import '~@angular/material/theming';
@mixin membership-theme($theme) {
/* stylelint-disable */
$primary: map-get($theme, primary);
$primary-dark: mat-color($primary, A900);
$accent: map-get($theme, accent);
$accent-color: mat-color($accent, 500);
/* stylelint-enable */
.membership-groups {
.header {
display: block;
margin-bottom: 1rem;
font-weight: 400;
margin-top: 0;
}
.sub-header {
font-size: .8rem;
color: #8795a1;
}
.people {
display: flex;
align-items: center;
flex-wrap: wrap;
margin-bottom: 1rem;
.owner {
margin-right: 1rem;
}
.img-list {
width: 100%;
margin-top: .5rem;
margin-left: 1rem;
display: flex;
align-items: center;
.spinner {
margin-left: -15px;
margin-right: 20px;
}
.add-img {
float: left;
margin: 0 8px 0 -15px;
}
.avatar-circle {
float: left;
margin: 0 8px 0 -12px;
height: 40px;
width: 40px;
border-radius: 50%;
-webkit-box-shadow: 2px 0 7px -1px rgba(33, 34, 36, .5);
-moz-box-shadow: 2px 0 7px -1px rgba(33, 34, 36, .5);
box-shadow: 2px 0 7px -1px rgba(33, 34, 36, .5);
display: flex;
align-items: center;
justify-content: center;
.membership-avatar {
display: flex;
align-items: center;
justify-content: center;
text-transform: uppercase;
color: white;
outline: none;
padding: 3px;
text-align: center;
height: 40px;
width: 40px;
box-sizing: border-box;
font-size: 8px;
border-radius: .5rem;
transition: background-color .2s ease-in-out;
background-color: $accent-color;
cursor: pointer;
flex-direction: column;
overflow: hidden;
span {
max-width: 30px;
text-overflow: ellipsis;
font-weight: 800;
display: block;
white-space: nowrap;
overflow: hidden;
}
i {
font-size: 1.2rem;
flex-basis: 100%;
width: 100%;
height: 1.2rem;
max-height: 1.2rem;
}
}
}
.margin-neg {
margin-left: -1rem;
}
}
}
}
}
@@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { MembershipsComponent } from './memberships.component';
describe('MembershipsComponent', () => {
let component: MembershipsComponent;
let fixture: ComponentFixture<MembershipsComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [MembershipsComponent],
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(MembershipsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,181 @@
import { animate, animateChild, keyframes, query, stagger, style, transition, trigger } from '@angular/animations';
import { Component, Input, OnInit } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { Router } from '@angular/router';
import { CreationType, MemberCreateDialogComponent } from 'src/app/modules/add-member-dialog/member-create-dialog.component';
import { MemberType, User, UserMembershipSearchResponse } from 'src/app/proto/generated/management_pb';
import { AdminService } from 'src/app/services/admin.service';
import { MgmtUserService } from 'src/app/services/mgmt-user.service';
import { OrgService } from 'src/app/services/org.service';
import { ProjectService } from 'src/app/services/project.service';
import { ToastService } from 'src/app/services/toast.service';
@Component({
selector: 'app-memberships',
templateUrl: './memberships.component.html',
styleUrls: ['./memberships.component.scss'],
animations: [
trigger('cardAnimation', [
transition('* => *', [
query('@animate', stagger('40ms', animateChild()), { optional: true }),
]),
]),
trigger('animate', [
transition(':enter', [
animate('.2s ease-in', keyframes([
style({ opacity: 0, offset: 0 }),
style({ opacity: .5, transform: 'scale(1.05)', offset: 0.3 }),
style({ opacity: 1, transform: 'scale(1)', offset: 1 }),
])),
]),
]),
],
})
export class MembershipsComponent implements OnInit {
public loading: boolean = false;
public memberships!: UserMembershipSearchResponse.AsObject;
@Input() public user!: User.AsObject;
public MemberType: any = MemberType;
constructor(
private orgService: OrgService,
private projectService: ProjectService,
private mgmtUserService: MgmtUserService,
private adminService: AdminService,
private dialog: MatDialog,
private toast: ToastService,
private router: Router,
) { }
ngOnInit(): void {
this.loadManager(this.user.id);
}
public async loadManager(userId: string): Promise<void> {
this.mgmtUserService.SearchUserMemberships(userId, 100, 0, []).then(response => {
this.memberships = response.toObject();
this.loading = false;
});
}
public navigateToObject(): void {
this.router.navigate(['/users', this.user.id, 'memberships']);
}
public addMember(): void {
const dialogRef = this.dialog.open(MemberCreateDialogComponent, {
width: '400px',
data: {
user: this.user,
},
});
dialogRef.afterClosed().subscribe(resp => {
if (resp && resp.creationType !== undefined) {
switch (resp.creationType) {
case CreationType.IAM:
this.createIamMember(resp);
break;
case CreationType.ORG:
this.createOrgMember(resp);
break;
case CreationType.PROJECT_OWNED:
this.createOwnedProjectMember(resp);
break;
case CreationType.PROJECT_GRANTED:
this.createGrantedProjectMember(resp);
break;
}
}
});
}
public createIamMember(response: any): void {
const users: User.AsObject[] = response.users;
const roles: string[] = response.roles;
if (users && users.length && roles && roles.length) {
Promise.all(users.map(user => {
return this.adminService.AddIamMember(user.id, roles);
})).then(() => {
this.toast.showInfo('IAM.TOAST.MEMBERADDED', true);
}).catch(error => {
this.toast.showError(error);
});
}
}
private createOrgMember(response: any): void {
const users: User.AsObject[] = response.users;
const roles: string[] = response.roles;
if (users && users.length && roles && roles.length) {
Promise.all(users.map(user => {
return this.orgService.AddMyOrgMember(user.id, roles);
})).then(() => {
this.toast.showInfo('ORG.TOAST.MEMBERADDED', true);
}).catch(error => {
this.toast.showError(error);
});
}
}
private createGrantedProjectMember(response: any): void {
const users: User.AsObject[] = response.users;
const roles: string[] = response.roles;
if (users && users.length && roles && roles.length) {
users.forEach(user => {
return this.projectService.AddProjectGrantMember(
response.projectId,
response.grantId,
user.id,
roles,
).then(() => {
this.toast.showInfo('PROJECT.TOAST.MEMBERADDED', true);
}).catch(error => {
this.toast.showError(error);
});
});
}
}
private createOwnedProjectMember(response: any): void {
const users: User.AsObject[] = response.users;
const roles: string[] = response.roles;
if (users && users.length && roles && roles.length) {
users.forEach(user => {
return this.projectService.AddProjectMember(response.projectId, user.id, roles)
.then(() => {
this.toast.showInfo('PROJECT.TOAST.MEMBERADDED', true);
}).catch(error => {
this.toast.showError(error);
});
});
}
}
getColor(type: MemberType): string {
const gen = type.toString();
const colors = [
'rgb(201, 115, 88)',
'rgb(226, 176, 50)',
'rgb(112, 89, 152)',
];
let hash = 0;
if (gen.length === 0) {
return colors[hash];
}
for (let i = 0; i < gen.length; i++) {
// tslint:disable-next-line: no-bitwise
hash = gen.charCodeAt(i) + ((hash << 5) - hash);
// tslint:disable-next-line: no-bitwise
hash = hash & hash;
}
hash = ((hash % colors.length) + colors.length) % colors.length;
return colors[hash];
}
}
@@ -48,6 +48,15 @@ const routes: Routes = [
animation: 'AddPage',
},
},
{
path: ':id/memberships',
loadChildren: () => import('./membership-detail/membership-detail.module').then(m => m.MembershipDetailModule),
canActivate: [AuthGuard, RoleGuard],
data: {
roles: ['user.membership.read'],
animation: 'AddPage',
},
},
];
@NgModule({
@@ -15,6 +15,7 @@ import { TranslateModule } from '@ngx-translate/core';
import { QRCodeModule } from 'angularx-qrcode';
import { CopyToClipboardModule } from 'src/app/directives/copy-to-clipboard/copy-to-clipboard.module';
import { HasRoleModule } from 'src/app/directives/has-role/has-role.module';
import { MemberCreateDialogModule } from 'src/app/modules/add-member-dialog/member-create-dialog.module';
import { CardModule } from 'src/app/modules/card/card.module';
import { ChangesModule } from 'src/app/modules/changes/changes.module';
import { DetailLayoutModule } from 'src/app/modules/detail-layout/detail-layout.module';
@@ -31,6 +32,7 @@ import { CodeDialogComponent } from './auth-user-detail/code-dialog/code-dialog.
import { DialogOtpComponent } from './auth-user-detail/dialog-otp/dialog-otp.component';
import { ThemeSettingComponent } from './auth-user-detail/theme-setting/theme-setting.component';
import { DetailFormModule } from './detail-form/detail-form.module';
import { MembershipsComponent } from './memberships/memberships.component';
import { PasswordComponent } from './password/password.component';
import { UserDetailRoutingModule } from './user-detail-routing.module';
import { UserDetailComponent } from './user-detail/user-detail.component';
@@ -46,6 +48,7 @@ import { UserMfaComponent } from './user-detail/user-mfa/user-mfa.component';
ThemeSettingComponent,
PasswordComponent,
CodeDialogComponent,
MembershipsComponent,
],
imports: [
UserDetailRoutingModule,
@@ -76,6 +79,7 @@ import { UserMfaComponent } from './user-detail/user-mfa/user-mfa.component';
CopyToClipboardModule,
DetailLayoutModule,
PasswordComplexityViewModule,
MemberCreateDialogModule,
],
})
export class UserDetailModule { }
@@ -4,7 +4,7 @@
<a (click)="navigateBack()" mat-icon-button>
<mat-icon class="icon">arrow_back</mat-icon>
</a>
<h1>{{ 'USER.PROFILE.TITLE' | translate }} {{user?.displayName}}</h1>
<h1>{{user?.displayName}}</h1>
<span class="fill-space"></span>
@@ -158,6 +158,8 @@
</div>
</div>
<app-memberships [user]="user"></app-memberships>
<app-changes class="changes" [changeType]="ChangeType.USER" [id]="user.id"></app-changes>
</div>
</app-meta-layout>
@@ -11,10 +11,7 @@
}
h1 {
font-size: 2rem;
margin: 0;
font-weight: normal;
margin-right: 1rem;
}
.fill-space {
@@ -13,8 +13,8 @@ import {
UserState,
UserView,
} from 'src/app/proto/generated/management_pb';
import { AuthUserService } from 'src/app/services/auth-user.service';
import { MgmtUserService } from 'src/app/services/mgmt-user.service';
import { ProjectService } from 'src/app/services/project.service';
import { ToastService } from 'src/app/services/toast.service';
@Component({
@@ -43,7 +43,7 @@ export class UserDetailComponent implements OnInit, OnDestroy {
private toast: ToastService,
private mgmtUserService: MgmtUserService,
private _location: Location,
public authUserService: AuthUserService,
public projectService: ProjectService,
) { }
public ngOnInit(): void {
@@ -2,7 +2,8 @@
<h1>{{ 'USER.PAGES.LIST' | translate }}</h1>
<p class="sub">{{ 'USER.PAGES.DESCRIPTION' | translate }}</p>
<app-refresh-table [loading]="loading$ | async" (refreshed)="refreshPage()" [dataSize]="dataSource.data.length">
<app-refresh-table [loading]="loading$ | async" (refreshed)="refreshPage()" [dataSize]="dataSource.data.length"
[timestamp]="userResult?.viewTimestamp">
<ng-template appHasRole [appHasRole]="['user.write']" actions>
<button (click)="deactivateSelectedUsers()" matTooltip="{{'ORG_DETAIL.TABLE.DEACTIVATE' | translate}}"
class="icon-button" mat-icon-button *ngIf="selection.hasValue()">
@@ -77,7 +77,7 @@ export class UserListComponent implements OnDestroy {
this.loadingSubject.next(true);
this.userService.SearchUsers(limit, offset).then(resp => {
this.userResult = resp.toObject();
this.dataSource.data = resp.toObject().resultList;
this.dataSource.data = this.userResult.resultList;
this.loadingSubject.next(false);
}).catch(error => {
this.toast.showError(error);