mirror of
https://github.com/zitadel/zitadel.git
synced 2025-10-28 07:06:25 +00:00
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:
@@ -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)),
|
||||
|
||||
@@ -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)),
|
||||
|
||||
@@ -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>
|
||||
@@ -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%;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 { }
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user