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 <florian@caos.ch>

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

Co-authored-by: Florian Forster <florian@caos.ch>

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

Co-authored-by: Florian Forster <florian@caos.ch>

* membership i18n

Co-authored-by: Florian Forster <florian@caos.ch>
This commit is contained in:
Max Peintner
2020-08-24 08:48:47 +02:00
committed by GitHub
parent d49fee23bf
commit 193cfb45f6
84 changed files with 1972 additions and 2911 deletions

View File

@@ -1,4 +1,4 @@
import { Component, ViewChild } from '@angular/core';
import { AfterViewInit, Component, ViewChild } from '@angular/core';
import { MatPaginator } from '@angular/material/paginator';
import { MatTableDataSource } from '@angular/material/table';
import { BehaviorSubject, from, Observable, of } from 'rxjs';
@@ -12,7 +12,9 @@ import { ToastService } from 'src/app/services/toast.service';
templateUrl: './failed-events.component.html',
styleUrls: ['./failed-events.component.scss'],
})
export class FailedEventsComponent {
export class FailedEventsComponent implements AfterViewInit {
// public viewTimestamp!: Timestamp.AsObject;
@ViewChild(MatPaginator) public eventPaginator!: MatPaginator;
public eventDataSource!: MatTableDataSource<FailedEvent.AsObject>;
@@ -24,11 +26,19 @@ export class FailedEventsComponent {
this.loadEvents();
}
ngAfterViewInit(): void {
this.loadEvents();
}
public loadEvents(): void {
this.loadingSubject.next(true);
from(this.adminService.GetFailedEvents()).pipe(
map(resp => {
return resp.toObject().failedEventsList;
const response = resp.toObject();
// if (response.viewTimestamp) {
// this.viewTimestamp = response.viewTimestamp;
// }
return response.failedEventsList;
}),
catchError(() => of([])),
finalize(() => this.loadingSubject.next(false)),

View File

@@ -1,4 +1,5 @@
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 { IamMemberView } from 'src/app/proto/generated/admin_pb';
@@ -11,6 +12,7 @@ import { AdminService } from 'src/app/services/admin.service';
*/
export class IamMembersDataSource extends DataSource<IamMemberView.AsObject> {
public totalResult: number = 0;
public viewTimestamp!: Timestamp.AsObject;
public membersSubject: BehaviorSubject<IamMemberView.AsObject[]> = new BehaviorSubject<IamMemberView.AsObject[]>([]);
private loadingSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
public loading$: Observable<boolean> = this.loadingSubject.asObservable();
@@ -27,8 +29,12 @@ export class IamMembersDataSource extends DataSource<IamMemberView.AsObject> {
from(this.adminService.SearchIamMembers(pageSize, offset)).pipe(
map(resp => {
this.totalResult = resp.toObject().totalResult;
return resp.toObject().resultList;
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)),

View File

@@ -1,99 +1,88 @@
<app-detail-layout [backRouterLink]="[ '/iam']" title="{{ 'IAM.MEMBER.TITLE' | translate }}"
description="{{ 'IAM.MEMBER.DESCRIPTION' | translate }}">
<div class="table-header-row">
<div class="col">
<ng-container *ngIf="!selection.hasValue()">
<span class="desc">{{'ORG_DETAIL.TABLE.TOTAL' | translate}}</span>
<span class="count">{{dataSource?.membersSubject.value.length}}</span>
</ng-container>
<ng-container *ngIf="selection.hasValue()">
<span class="desc">{{'ORG_DETAIL.TABLE.SELECTION' | translate}}</span>
<span class="count">{{selection?.selected?.length}}</span>
</ng-container>
</div>
<span class="fill-space"></span>
<ng-template appHasRole [appHasRole]="['iam.member.delete']">
<app-refresh-table (refreshed)="refreshPage()" [dataSize]="dataSource.totalResult"
[timestamp]="dataSource?.viewTimestamp" [selection]="selection" [loading]="dataSource.loading$ | async">
<ng-template appHasRole actions [appHasRole]="['iam.member.delete']">
<button color="warn" (click)="removeProjectMemberSelection()"
matTooltip="{{'ORG_DETAIL.TABLE.DELETE' | translate}}" class="icon-button" mat-icon-button
*ngIf="selection.hasValue()">
<i class="las la-trash"></i>
</button>
</ng-template>
<ng-template appHasRole [appHasRole]="['iam.member.write']">
<ng-template appHasRole actions [appHasRole]="['iam.member.write']">
<a color="primary" [disabled]="disabled" class="add-button" (click)="openAddMember()" color="primary"
mat-raised-button>
<mat-icon class="icon">add</mat-icon>{{ 'ACTIONS.NEW' | translate }}
</a>
</ng-template>
</div>
<div class="table-wrapper">
<div class="spinner-container" *ngIf="dataSource?.loading$ | async">
<mat-spinner diameter="50"></mat-spinner>
<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="firstname">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.MEMBER.FIRSTNAME' | translate }} </th>
<td class="pointer" [routerLink]="['/user', member.userId]" mat-cell *matCellDef="let member">
{{member.firstName}} </td>
</ng-container>
<ng-container matColumnDef="lastname">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.MEMBER.LASTNAME' | translate }} </th>
<td class="pointer" [routerLink]="['/user', member.userId]" mat-cell *matCellDef="let member">
{{member.lastName}} </td>
</ng-container>
<ng-container matColumnDef="username">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.MEMBER.USERNAME' | translate }} </th>
<td class="pointer" [routerLink]="['/user', member.userId]" mat-cell *matCellDef="let member">
{{member.userName}} </td>
</ng-container>
<ng-container matColumnDef="email">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.MEMBER.EMAIL' | translate }} </th>
<td class="pointer" [routerLink]="['/user', member.userId]" mat-cell *matCellDef="let member">
{{member.email}}
</td>
</ng-container>
<ng-container matColumnDef="roles">
<th mat-header-cell *matHeaderCellDef> {{ 'ROLESLABEL' | translate }} </th>
<td class="pointer" mat-cell *matCellDef="let member">
<mat-form-field class="form-field" appearance="outline">
<mat-label>{{ 'ROLESLABEL' | translate }}</mat-label>
<mat-select [(ngModel)]="member.rolesList" multiple
[disabled]="(['org.member.write'] | hasRole | async) == false"
(selectionChange)="updateRoles(member, $event)">
<mat-option *ngFor="let role of memberRoleOptions" [value]="role">
{{ role }}
</mat-option>
</mat-select>
</mat-form-field>
</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="background-style paginator" #paginator [pageSize]="50"
[pageSizeOptions]="[25, 50, 100, 250]">
</mat-paginator>
</div>
<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="firstname">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.MEMBER.FIRSTNAME' | translate }} </th>
<td class="pointer" [routerLink]="['/user', member.userId]" mat-cell *matCellDef="let member">
{{member.firstName}} </td>
</ng-container>
<ng-container matColumnDef="lastname">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.MEMBER.LASTNAME' | translate }} </th>
<td class="pointer" [routerLink]="['/user', member.userId]" mat-cell *matCellDef="let member">
{{member.lastName}} </td>
</ng-container>
<ng-container matColumnDef="username">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.MEMBER.USERNAME' | translate }} </th>
<td class="pointer" [routerLink]="['/user', member.userId]" mat-cell *matCellDef="let member">
{{member.userName}} </td>
</ng-container>
<ng-container matColumnDef="email">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.MEMBER.EMAIL' | translate }} </th>
<td class="pointer" [routerLink]="['/user', member.userId]" mat-cell *matCellDef="let member">
{{member.email}}
</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">
<mat-form-field class="form-field" appearance="outline">
<mat-label>{{ 'PROJECT.GRANT.TITLE' | translate }}</mat-label>
<mat-select [(ngModel)]="member.rolesList" multiple
[disabled]="(['org.member.write'] | hasRole | async) == false"
(selectionChange)="updateRoles(member, $event)">
<mat-option *ngFor="let role of memberRoleOptions" [value]="role">
{{ 'ROLES.'+role | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</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="background-style paginator" #paginator [pageSize]="50"
[pageSizeOptions]="[25, 50, 100, 250]">
</mat-paginator>
</div>
</app-refresh-table>
</app-detail-layout>

View File

@@ -1,46 +1,11 @@
.table-header-row {
display: flex;
align-items: center;
width: 100%;
.col {
display: flex;
flex-direction: column;
.desc {
font-size: .8rem;
color: #8795a1;
}
.count {
font-size: 2rem;
}
}
.fill-space {
flex: 1;
}
.icon-button {
margin-right: .5rem;
}
.add-button {
border-radius: .5rem;
}
.add-button {
border-radius: .5rem;
}
.table-wrapper {
overflow: auto;
width: 100%;
.spinner-container {
display: flex;
align-items: center;
justify-content: center;
}
.table,
.paginator {
width: 100%;

View File

@@ -105,7 +105,7 @@ export class IamMembersComponent implements AfterViewInit {
public openAddMember(): void {
const dialogRef = this.dialog.open(MemberCreateDialogComponent, {
data: {
creationType: CreationType.ORG,
creationType: CreationType.IAM,
},
width: '400px',
});
@@ -127,4 +127,9 @@ export class IamMembersComponent implements AfterViewInit {
}
});
}
public refreshPage(): void {
this.selection.clear();
this.dataSource.loadMembers(this.paginator.pageIndex, this.paginator.pageSize);
}
}

View File

@@ -16,6 +16,7 @@ import { MatTooltipModule } from '@angular/material/tooltip';
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 { HasRolePipeModule } from 'src/app/pipes/has-role-pipe.module';
import { IamMembersRoutingModule } from './iam-members-routing.module';
@@ -45,6 +46,7 @@ import { IamMembersComponent } from './iam-members.component';
MatFormFieldModule,
MatSelectModule,
HasRolePipeModule,
RefreshTableModule,
],
})
export class IamMembersModule { }

View File

@@ -1,4 +1,4 @@
import { Component, ViewChild } from '@angular/core';
import { AfterViewInit, Component, ViewChild } from '@angular/core';
import { MatPaginator } from '@angular/material/paginator';
import { MatTableDataSource } from '@angular/material/table';
import { BehaviorSubject, from, Observable, of } from 'rxjs';
@@ -11,7 +11,7 @@ import { AdminService } from 'src/app/services/admin.service';
templateUrl: './iam-views.component.html',
styleUrls: ['./iam-views.component.scss'],
})
export class IamViewsComponent {
export class IamViewsComponent implements AfterViewInit {
@ViewChild(MatPaginator) public paginator!: MatPaginator;
public dataSource!: MatTableDataSource<View.AsObject>;
@@ -23,6 +23,10 @@ export class IamViewsComponent {
this.loadViews();
}
ngAfterViewInit(): void {
this.loadViews();
}
public loadViews(): void {
this.loadingSubject.next(true);
from(this.adminService.GetViews()).pipe(