mirror of
https://github.com/andwati/zitadel.git
synced 2026-11-12 00:55:12 +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 <[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:
co-authored by
Florian Forster
parent
d49fee23bf
commit
193cfb45f6
+2
@@ -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>
|
||||
+58
@@ -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();
|
||||
}
|
||||
}
|
||||
+74
@@ -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>
|
||||
+51
@@ -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;
|
||||
}
|
||||
+25
@@ -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();
|
||||
});
|
||||
});
|
||||
+213
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user