feat: rehauled console (#3525)

* new console

* move npm ci to angular build

* rel path for assets

* local grpc copy

* login policy, rm clear views, features rel path

* lock

Co-authored-by: Livio Amstutz <[email protected]>
This commit is contained in:
Max Peintner
2022-04-28 10:35:02 +00:00
committed by GitHub
co-authored by Livio Amstutz
parent 00f7dbe875
commit 08ae39ae19
668 changed files with 47746 additions and 19117 deletions
@@ -1,97 +1,102 @@
<cnsl-refresh-table *ngIf="dataSource" (refreshed)="changePage()" [dataSize]="dataSource.totalResult"
[timestamp]="dataSource.viewTimestamp" [selection]="selection" [loading]="dataSource?.loading$ | async">
[timestamp]="dataSource.viewTimestamp" [selection]="selection" [loading]="dataSource?.loading$ | async">
<ng-container actions *ngIf="selection.hasValue()">
<ng-content select="[selectactions]"></ng-content>
</ng-container>
<ng-container actions *ngIf="selection.hasValue()">
<ng-content select="[selectactions]"></ng-content>
</ng-container>
<div actions>
<ng-content select="[writeactions]"></ng-content>
</div>
<div actions *ngIf="!selection.hasValue()">
<ng-content select="[writeactions]"></ng-content>
</div>
<div class="table-wrapper">
<table mat-table class="table" aria-label="Elements" [dataSource]="dataSource">
<ng-container matColumnDef="select">
<th class="selection" mat-header-cell *matHeaderCellDef>
<mat-checkbox [disabled]="!canWrite" 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 [disabled]="!canWrite" color="primary" (click)="$event.stopPropagation()"
(change)="$event ? selection.toggle(row) : null" [checked]="selection.isSelected(row)">
<cnsl-avatar *ngIf="row?.displayName && row.firstName && row.lastName; else cog" class="avatar"
[name]="row.displayName" [avatarUrl]="row.avatarUrl || ''" [avatarUrl]="row.avatarUrl|| ''" [forColor]="row?.preferredLoginName" [size]="32">
</cnsl-avatar>
<ng-template #cog>
<div class="sa-icon">
<i class="las la-user-cog"></i>
</div>
</ng-template>
</mat-checkbox>
</td>
</ng-container>
<div class="table-wrapper">
<table mat-table class="table" aria-label="Elements" [dataSource]="dataSource">
<ng-container matColumnDef="select">
<th class="selection" mat-header-cell *matHeaderCellDef>
<mat-checkbox [disabled]="!canWrite" 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 [disabled]="!canWrite" color="primary" (click)="$event.stopPropagation()"
(change)="$event ? selection.toggle(row) : null" [checked]="selection.isSelected(row)">
<cnsl-avatar *ngIf="row?.displayName && row.firstName && row.lastName; else cog" class="avatar"
[name]="row.displayName" [avatarUrl]="row.avatarUrl || ''" [avatarUrl]="row.avatarUrl|| ''"
[forColor]="row?.preferredLoginName" [size]="32">
</cnsl-avatar>
<ng-template #cog>
<cnsl-avatar [forColor]="row.preferredLoginName" [isMachine]="true">
<i class="las la-robot"></i>
</cnsl-avatar>
</ng-template>
</mat-checkbox>
</td>
</ng-container>
<ng-container matColumnDef="userId">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.MEMBER.USERID' | translate }} </th>
<td class="pointer" [routerLink]="['/users', member.userId]" mat-cell *matCellDef="let member">
{{member.userId}} </td>
</ng-container>
<ng-container matColumnDef="userId">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.MEMBER.USERID' | translate }} </th>
<td mat-cell *matCellDef="let member">
{{member.userId}} </td>
</ng-container>
<ng-container matColumnDef="firstname">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.MEMBER.FIRSTNAME' | translate }} </th>
<td class="pointer" [routerLink]="['/users', member.userId]" mat-cell *matCellDef="let member">
{{member.firstName}} </td>
</ng-container>
<ng-container matColumnDef="displayName">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.MEMBER.DISPLAYNAME' | translate }} </th>
<td mat-cell *matCellDef="let member">
{{member.displayName}} </td>
</ng-container>
<ng-container matColumnDef="lastname">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.MEMBER.LASTNAME' | translate }} </th>
<td class="pointer" [routerLink]="['/users', member.userId]" mat-cell *matCellDef="let member">
{{member.lastName}} </td>
</ng-container>
<ng-container matColumnDef="loginname">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.MEMBER.LOGINNAME' | translate }} </th>
<td mat-cell *matCellDef="let member">
{{member.preferredLoginName}} </td>
</ng-container>
<ng-container matColumnDef="loginname">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.MEMBER.LOGINNAME' | translate }} </th>
<td class="pointer" [routerLink]="['/users', member.userId]" mat-cell *matCellDef="let member">
{{member.preferredloginname}} </td>
</ng-container>
<ng-container matColumnDef="email">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.MEMBER.EMAIL' | translate }} </th>
<td mat-cell *matCellDef="let member">
{{member.email}}
</td>
</ng-container>
<ng-container matColumnDef="email">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.MEMBER.EMAIL' | translate }} </th>
<td class="pointer" [routerLink]="['/users', member.userId]" mat-cell *matCellDef="let member">
{{member.email}}
</td>
</ng-container>
<ng-container matColumnDef="actions" stickyEnd>
<th mat-header-cell *matHeaderCellDef></th>
<td mat-cell *matCellDef="let member" class="member-action-tr">
<cnsl-table-actions [hasActions]="true">
<button actions matTooltip="{{'ACTIONS.REMOVE' | translate}}" color="warn"
(click)="triggerDeleteMember(member)" mat-icon-button><i class="las la-trash"></i></button>
<ng-container matColumnDef="actions" stickyEnd>
<th mat-header-cell *matHeaderCellDef></th>
<td mat-cell *matCellDef="let view">
<button matTooltip="{{'ACTIONS.REMOVE' | translate}}" color="warn"
(click)="triggerDeleteMember(view)" mat-icon-button><i class="las la-trash"></i></button>
</td>
</ng-container>
<button menuActions mat-menu-item [routerLink]="['/users', member.userId]">
{{'ACTIONS.TABLE.SHOWUSER' | translate : ({value: member.displayName})}}
</button>
</cnsl-table-actions>
</td>
</ng-container>
<ng-container matColumnDef="roles">
<th mat-header-cell *matHeaderCellDef class="role-row"> {{ 'ROLESLABEL' | translate }} </th>
<td mat-cell *matCellDef="let member" class="role-row">
<cnsl-form-field class="form-field" appearance="outline">
<mat-select [(ngModel)]="member.rolesList" multiple [disabled]="!canWrite"
(selectionChange)="updateRoles.emit({member: member, change: $event})">
<mat-option *ngFor="let role of memberRoleOptions" [value]="role">
{{ role }}
</mat-option>
</mat-select>
</cnsl-form-field>
</td>
</ng-container>
<ng-container matColumnDef="roles">
<th mat-header-cell *matHeaderCellDef class="role-row"> {{ 'ROLESLABEL' | translate }} </th>
<td mat-cell *matCellDef="let member" class="role-row">
<mat-chip-list class="cnsl-chip-list" aria-label="role selection">
<mat-chip class="cnsl-chip" *ngFor="let role of member.rolesList" [removable]="true" [selectable]="false"
(removed)="removeRole(member, role)">
<div class="cnsl-chip-dot" [style.background]="getColor(role)"></div>
<span>{{role | roletransform}}</span>
<button matChipRemove>
<mat-icon>cancel</mat-icon>
</button>
</mat-chip>
</mat-chip-list>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr class="highlight" mat-row *matRowDef="let row; columns: displayedColumns;">
</tr>
</table>
</div>
<cnsl-paginator *ngIf="dataSource" class="paginator" #paginator [timestamp]="dataSource?.viewTimestamp" [pageSize]="INITIALPAGESIZE"
[length]="dataSource.totalResult" [pageSizeOptions]="[25, 50, 100, 250]" (page)="changePage($event)">
</cnsl-paginator>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr class="highlight pointer" (click)="addRole(member)" mat-row
*matRowDef="let member; columns: displayedColumns;">
</tr>
</table>
</div>
<cnsl-paginator *ngIf="dataSource" class="paginator" #paginator [timestamp]="dataSource?.viewTimestamp"
[pageSize]="INITIALPAGESIZE" [length]="dataSource.totalResult" [pageSizeOptions]="[25, 50, 100, 250]"
(page)="changePage($event)">
</cnsl-paginator>
</cnsl-refresh-table>
@@ -1,53 +1,36 @@
.icon-button {
margin-right: .5rem;
margin-right: 0.5rem;
}
.table-wrapper {
overflow-x: auto;
td,
th {
padding: 0.5rem;
.table,
.paginator {
width: 100%;
td,
th {
padding: .5rem;
&:first-child {
padding-left: 0;
padding-right: 1rem;
}
&:last-child {
padding-right: 0;
}
}
.action {
width: 40px;
}
&:first-child {
padding-left: 0;
padding-right: 1rem;
}
tr {
button {
visibility: hidden;
}
&:hover {
button {
visibility: visible;
}
}
&:last-child {
padding-right: 0;
}
}
.sa-icon {
display: block;
width: 32px;
margin: 0 .5rem;
.action {
width: 40px;
}
i {
margin: auto;
}
.member-action-tr {
max-width: 100px;
}
.sa-icon {
display: block;
width: 32px;
margin: 0 0.5rem;
i {
margin: auto;
}
}
@@ -1,5 +1,4 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { MatPaginatorModule } from '@angular/material/paginator';
import { MatSortModule } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
@@ -7,28 +6,25 @@ import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { MembersTableComponent } from './members-table.component';
describe('MembersTableComponent', () => {
let component: MembersTableComponent;
let fixture: ComponentFixture<MembersTableComponent>;
let component: MembersTableComponent;
let fixture: ComponentFixture<MembersTableComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [MembersTableComponent],
imports: [
NoopAnimationsModule,
MatPaginatorModule,
MatSortModule,
MatTableModule,
],
}).compileComponents();
}));
beforeEach(
waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [MembersTableComponent],
imports: [NoopAnimationsModule, MatSortModule, MatTableModule],
}).compileComponents();
}),
);
beforeEach(() => {
fixture = TestBed.createComponent(MembersTableComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
beforeEach(() => {
fixture = TestBed.createComponent(MembersTableComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should compile', () => {
expect(component).toBeTruthy();
});
it('should compile', () => {
expect(component).toBeTruthy();
});
});
@@ -1,6 +1,6 @@
import { SelectionModel } from '@angular/cdk/collections';
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output, ViewChild } from '@angular/core';
import { MatSelectChange } from '@angular/material/select';
import { MatDialog } from '@angular/material/dialog';
import { MatTable } from '@angular/material/table';
import { Observable, Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
@@ -10,11 +10,18 @@ import {
ProjectGrantMembersDataSource,
} from 'src/app/pages/projects/owned-projects/project-grant-detail/project-grant-members-datasource';
import { Member } from 'src/app/proto/generated/zitadel/member_pb';
import { getMembershipColor } from 'src/app/utils/color';
import { AddMemberRolesDialogComponent } from '../add-member-roles-dialog/add-member-roles-dialog.component';
import { PageEvent, PaginatorComponent } from '../paginator/paginator.component';
import { ProjectMembersDataSource } from '../project-members/project-members-datasource';
import { WarnDialogComponent } from '../warn-dialog/warn-dialog.component';
type MemberDatasource = OrgMembersDataSource | ProjectMembersDataSource | ProjectGrantMembersDataSource | IamMembersDataSource;
type MemberDatasource =
| OrgMembersDataSource
| ProjectMembersDataSource
| ProjectGrantMembersDataSource
| IamMembersDataSource;
@Component({
selector: 'cnsl-members-table',
@@ -32,17 +39,15 @@ export class MembersTableComponent implements OnInit, OnDestroy {
@Input() public memberRoleOptions: string[] = [];
@Input() public factoryLoadFunc!: Function;
@Input() public refreshTrigger!: Observable<void>;
@Output() public updateRoles: EventEmitter<{ member: Member.AsObject, change: MatSelectChange; }> = new EventEmitter();
@Output() public updateRoles: EventEmitter<{ member: Member.AsObject; change: string[] }> = new EventEmitter();
@Output() public changedSelection: EventEmitter<any[]> = new EventEmitter();
@Output() public deleteMember: EventEmitter<Member.AsObject> = new EventEmitter();
private destroyed: Subject<void> = new Subject();
public displayedColumns: string[] = ['select', 'userId', 'displayName', 'loginname', 'email', 'roles'];
/** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */
public displayedColumns: string[] = ['select', 'userId', 'firstname', 'lastname', 'loginname', 'email', 'roles'];
constructor() {
this.selection.changed.pipe(takeUntil(this.destroyed)).subscribe(_ => {
constructor(private dialog: MatDialog) {
this.selection.changed.pipe(takeUntil(this.destroyed)).subscribe((_) => {
this.changedSelection.emit(this.selection.selected);
});
}
@@ -52,7 +57,7 @@ export class MembersTableComponent implements OnInit, OnDestroy {
this.changePage(this.paginator);
});
if (this.canDelete) {
if (this.canDelete || this.canWrite) {
this.displayedColumns.push('actions');
}
}
@@ -61,6 +66,52 @@ export class MembersTableComponent implements OnInit, OnDestroy {
this.destroyed.next();
}
public getColor(role: string) {
return getMembershipColor(role)[500];
}
public removeRole(member: Member.AsObject, role: string) {
const dialogRef = this.dialog.open(WarnDialogComponent, {
data: {
confirmKey: 'ACTIONS.DELETE',
cancelKey: 'ACTIONS.CANCEL',
titleKey: 'GRANTS.DIALOG.DELETE_TITLE',
descriptionKey: 'GRANTS.DIALOG.DELETE_DESCRIPTION',
},
width: '400px',
});
dialogRef.afterClosed().subscribe((resp) => {
if (resp) {
const newRoles = Object.assign([], member.rolesList);
const index = newRoles.findIndex((r) => r === role);
if (index > -1) {
newRoles.splice(index);
member.rolesList = newRoles;
this.updateRoles.emit({ member: member, change: newRoles });
}
}
});
}
public addRole(member: Member.AsObject) {
const dialogRef = this.dialog.open(AddMemberRolesDialogComponent, {
data: {
user: member.displayName,
allRoles: this.memberRoleOptions,
selectedRoles: member.rolesList,
},
width: '400px',
});
dialogRef.afterClosed().subscribe((resp) => {
if (resp && resp.length) {
member.rolesList = resp;
this.updateRoles.emit({ member: member, change: resp });
}
});
}
public isAllSelected(): boolean {
const numSelected = this.selection.selected.length;
const numRows = this.dataSource.membersSubject.value.length;
@@ -68,9 +119,9 @@ export class MembersTableComponent implements OnInit, OnDestroy {
}
public masterToggle(): void {
this.isAllSelected() ?
this.selection.clear() :
this.dataSource.membersSubject.value.forEach(row => this.selection.select(row));
this.isAllSelected()
? this.selection.clear()
: this.dataSource.membersSubject.value.forEach((row) => this.selection.select(row));
}
public changePage(event?: PageEvent): any {
@@ -79,6 +130,20 @@ export class MembersTableComponent implements OnInit, OnDestroy {
}
public triggerDeleteMember(member: any): void {
this.deleteMember.emit(member);
const dialogRef = this.dialog.open(WarnDialogComponent, {
data: {
confirmKey: 'ACTIONS.DELETE',
cancelKey: 'ACTIONS.CANCEL',
titleKey: 'MEMBER.DIALOG.DELETE_TITLE',
descriptionKey: 'MEMBER.DIALOG.DELETE_DESCRIPTION',
},
width: '400px',
});
dialogRef.afterClosed().subscribe((resp) => {
if (resp) {
this.deleteMember.emit(member);
}
});
}
}
@@ -3,6 +3,7 @@ import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatChipsModule } from '@angular/material/chips';
import { MatIconModule } from '@angular/material/icon';
import { MatSelectModule } from '@angular/material/select';
import { MatSortModule } from '@angular/material/sort';
@@ -11,35 +12,40 @@ import { MatTooltipModule } from '@angular/material/tooltip';
import { RouterModule } from '@angular/router';
import { TranslateModule } from '@ngx-translate/core';
import { InputModule } from 'src/app/modules/input/input.module';
import { RoleTransformPipeModule } from 'src/app/pipes/role-transform/role-transform.module';
import { AddMemberRolesDialogModule } from '../add-member-roles-dialog/add-member-roles-dialog.module';
import { AvatarModule } from '../avatar/avatar.module';
import { PaginatorModule } from '../paginator/paginator.module';
import { RefreshTableModule } from '../refresh-table/refresh-table.module';
import { TableActionsModule } from '../table-actions/table-actions.module';
import { WarnDialogModule } from '../warn-dialog/warn-dialog.module';
import { MembersTableComponent } from './members-table.component';
@NgModule({
declarations: [
MembersTableComponent,
],
imports: [
CommonModule,
InputModule,
MatSelectModule,
MatCheckboxModule,
MatIconModule,
MatTableModule,
PaginatorModule,
MatSortModule,
MatTooltipModule,
FormsModule,
TranslateModule,
RefreshTableModule,
RouterModule,
AvatarModule,
MatButtonModule,
],
exports: [
MembersTableComponent,
],
declarations: [MembersTableComponent],
imports: [
CommonModule,
InputModule,
MatSelectModule,
MatCheckboxModule,
MatIconModule,
MatTableModule,
MatChipsModule,
RoleTransformPipeModule,
PaginatorModule,
AddMemberRolesDialogModule,
MatSortModule,
MatTooltipModule,
FormsModule,
TranslateModule,
WarnDialogModule,
RefreshTableModule,
TableActionsModule,
RouterModule,
AvatarModule,
MatButtonModule,
],
exports: [MembersTableComponent],
})
export class MembersTableModule { }
export class MembersTableModule {}