feat(console): org managers, refactor, bugfixes (#226)

* resourceowner header, i18n

* password policy validators, roles required field

* use angular pattern instead of custom validator

* user detail fixes, mfa qr code, add org

* use mgmt for mfa list

* fetch owned projects

* search project

* seperate owned from granted projects

* lint

* fix granted project grid

* refactor project detail

* disable zitadel apps

* refactor project contributors

* changed i18n

* hide meta nav

* mobile meta layout

* refactor contributor name

* refactor org contributors

* org i18n, org member detail view
This commit is contained in:
Max Peintner
2020-06-16 14:52:07 +02:00
committed by GitHub
parent 7a6ca24625
commit f2cdae9ea3
95 changed files with 5019 additions and 3413 deletions
@@ -1,116 +0,0 @@
<div class="max-width-container">
<h1>{{ 'PROJECT.PAGES.LIST' | translate }}</h1>
<p class="sub">{{ 'PROJECT.PAGES.LISTDESCRIPTION' | translate }}</p>
<app-project-grid *ngIf="grid && projectList" [loading]="loading$ | async" (changedView)="grid = false"
[items]="projectList" (newClicked)="addProject()">
</app-project-grid>
<div *ngIf="!grid" class="view-toggle">
<button (click)="grid = true" mat-icon-button>
<i matTooltip="show grid view" class="las la-th-large"></i>
</button>
</div>
<div *ngIf="!grid && projectList">
<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?.data?.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>
<div @list class="action-btns" *ngIf="selection.hasValue()">
<button @animate (click)="deactivateSelectedProjects()"
matTooltip="{{'PROJECT.TABLE.DEACTIVATE' | translate}}" class="icon-button" mat-icon-button>
<mat-icon svgIcon="mdi_light_off"></mat-icon>
</button>
<button @animate (click)="reactivateSelectedProjects()"
matTooltip="{{'PROJECT.TABLE.ACTIVATE' | translate}}" class="icon-button" mat-icon-button>
<mat-icon svgIcon="mdi_light_on"></mat-icon>
</button>
</div>
<a class="add-button" [routerLink]="[ '/projects', 'create']" color="primary" mat-raised-button>
<mat-icon class="icon">add</mat-icon>{{ 'ACTIONS.NEW' | translate }}
</a>
</div>
<div class="table-wrapper">
<div class="spinner-container" *ngIf="loading$ | async">
<mat-spinner diameter="50"></mat-spinner>
</div>
<table class="background-style" mat-table [dataSource]="dataSource">
<ng-container matColumnDef="select">
<th class="selection" mat-header-cell *matHeaderCellDef>
<mat-checkbox (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 (click)="$event.stopPropagation()"
(change)="$event ? selection.toggle(row) : null" [checked]="selection.isSelected(row)">
</mat-checkbox>
</td>
</ng-container>
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.NAME' | translate }} </th>
<td mat-cell *matCellDef="let project"> {{project.name}} </td>
</ng-container>
<ng-container matColumnDef="orgName">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.TABLE.ORGNAME' | translate }} </th>
<td class="pointer" mat-cell *matCellDef="let project">
{{project.orgName}} </td>
</ng-container>
<ng-container matColumnDef="orgDomain">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.TABLE.ORGDOMAIN' | translate }} </th>
<td class="pointer" mat-cell *matCellDef="let project">
{{project?.orgDomain}} </td>
</ng-container>
<ng-container matColumnDef="state">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.TABLE.STATE' | translate }} </th>
<td mat-cell *matCellDef="let project"><span
*ngIf="project.state">{{'PROJECT.STATE.'+project.state | translate}}</span></td>
</ng-container>
<ng-container matColumnDef="type">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.TABLE.TYPE' | translate }} </th>
<td mat-cell *matCellDef="let project">
<span *ngIf="project.type !== undefined">{{'PROJECT.TYPE.'+project.type | translate}}</span>
</td>
</ng-container>
<ng-container matColumnDef="creationDate">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.TABLE.CREATIONDATE' | translate }} </th>
<td mat-cell *matCellDef="let project">
<span
*ngIf="project.creationDate">{{dateFromTimestamp(project.creationDate) | date: 'EEE dd. MMM, HH:mm'}}</span>
</td>
</ng-container>
<ng-container matColumnDef="changeDate">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.TABLE.CHANGEDATE' | translate }} </th>
<td mat-cell *matCellDef="let project">
<span
*ngIf="project.changeDate">{{dateFromTimestamp(project.changeDate) | date: 'EEE dd. MMM, HH:mm'}}</span>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr class="data-row" mat-row *matRowDef="let row; columns: displayedColumns;"
[routerLink]="['/projects', row.id]"></tr>
</table>
<mat-paginator class="background-style" [length]="totalResult" [pageSize]="10"
[pageSizeOptions]="[5, 10, 20]" (page)="changePage($event)"></mat-paginator>
</div>
</div>
</div>
@@ -1,107 +0,0 @@
h1 {
font-family: ailerons;
margin-top: 0;
}
.sub {
color: #81868a;
margin-bottom: 2rem;
}
.content {
display: flex;
flex-direction: row;
flex-wrap: wrap;
margin: 0 -.5rem;
mat-form-field {
flex: 1 1 33%;
margin: 0 .5rem;
}
}
.view-toggle {
width: 100%;
display: flex;
justify-content: flex-end;
button {
display: block;
border-radius: .5rem;
}
}
.table-header-row {
display: flex;
align-items: center;
.col {
display: flex;
flex-direction: column;
.desc {
font-size: .8rem;
color: #81868a;
}
.count {
font-size: 2rem;
}
}
.fill-space {
flex: 1;
}
.action-btns {
display: flex;
align-items: center;
}
.icon-button {
margin-right: .5rem;
}
.add-button {
border-radius: .5rem;
}
}
.table-wrapper {
overflow: auto;
.spinner-container {
display: flex;
align-items: center;
justify-content: center;
}
table, mat-paginator {
width: 100%;
background-color: #2d2e30;
td, th {
&:first-child {
padding-left: 0;
padding-right: 1rem;
}
&:last-child {
padding-right: 0;
}
}
.data-row {
cursor: pointer;
&:hover {
background-color: #ffffff05;
}
}
.selection {
width: 50px;
max-width: 50px;
}
}
}
tr {
outline: none;
}
@@ -1,25 +0,0 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ProjectListComponent } from './project-list.component';
describe('ProjectListComponent', () => {
let component: ProjectListComponent;
let fixture: ComponentFixture<ProjectListComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ProjectListComponent],
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ProjectListComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -1,130 +0,0 @@
import { animate, animateChild, query, stagger, style, transition, trigger } from '@angular/animations';
import { SelectionModel } from '@angular/cdk/collections';
import { Component, OnDestroy, OnInit } from '@angular/core';
import { PageEvent } from '@angular/material/paginator';
import { MatTableDataSource } from '@angular/material/table';
import { ActivatedRoute, Router } from '@angular/router';
import { TranslateService } from '@ngx-translate/core';
import { Timestamp } from 'google-protobuf/google/protobuf/timestamp_pb';
import { BehaviorSubject, Observable, Subscription } from 'rxjs';
import { GrantedProject, Project } from 'src/app/proto/generated/management_pb';
import { ProjectService } from 'src/app/services/project.service';
import { ToastService } from 'src/app/services/toast.service';
@Component({
selector: 'app-project-list',
templateUrl: './project-list.component.html',
styleUrls: ['./project-list.component.scss'],
animations: [
trigger('list', [
transition(':enter', [
query('@animate',
stagger(80, animateChild()),
),
]),
]),
trigger('animate', [
transition(':enter', [
style({ opacity: 0, transform: 'translateY(-100%)' }),
animate('100ms', style({ opacity: 1, transform: 'translateY(0)' })),
]),
transition(':leave', [
style({ opacity: 1, transform: 'translateY(0)' }),
animate('100ms', style({ opacity: 0, transform: 'translateY(100%)' })),
]),
]),
],
})
export class ProjectListComponent implements OnInit, OnDestroy {
public totalResult: number = 0;
public dataSource: MatTableDataSource<GrantedProject.AsObject> = new MatTableDataSource<GrantedProject.AsObject>();
public projectList: GrantedProject.AsObject[] = [];
public displayedColumns: string[] = ['select', 'name', 'orgName', 'orgDomain', 'type', 'state', 'creationDate', 'changeDate'];
public selection: SelectionModel<Project.AsObject> = new SelectionModel<Project.AsObject>(true, []);
private loadingSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
public loading$: Observable<boolean> = this.loadingSubject.asObservable();
public grid: boolean = true;
private subscription?: Subscription;
constructor(private router: Router,
public translate: TranslateService,
private route: ActivatedRoute,
private projectService: ProjectService,
private toast: ToastService,
) { }
public ngOnInit(): void {
this.subscription = this.route.params.subscribe(() => this.getData(10, 0));
}
public ngOnDestroy(): void {
this.subscription?.unsubscribe();
}
public isAllSelected(): boolean {
const numSelected = this.selection.selected.length;
const numRows = this.dataSource.data.length;
return numSelected === numRows;
}
public masterToggle(): void {
this.isAllSelected() ?
this.selection.clear() :
this.dataSource.data.forEach(row => this.selection.select(row));
}
public changePage(event: PageEvent): void {
this.getData(event.pageSize, event.pageIndex);
}
public addProject(): void {
this.router.navigate(['/projects', 'create']);
}
private async getData(limit: number, offset: number): Promise<void> {
console.log('getprojects');
this.loadingSubject.next(true);
this.projectService.SearchGrantedProjects(limit, offset).then(res => {
this.projectList = res.toObject().resultList;
this.totalResult = res.toObject().totalResult;
this.dataSource.data = this.projectList;
this.loadingSubject.next(false);
console.log(this.projectList);
}).catch(error => {
console.error(error);
this.toast.showError(error.message);
this.loadingSubject.next(false);
});
}
public dateFromTimestamp(date: Timestamp.AsObject): any {
const ts: Date = new Date(date.seconds * 1000 + date.nanos / 1000);
return ts;
}
public reactivateSelectedProjects(): void {
const promises = this.selection.selected.map(project => {
this.projectService.ReactivateProject(project.id);
});
Promise.all(promises).then(() => {
this.toast.showInfo('Reactivated selected projects successfully');
}).catch(error => {
this.toast.showInfo(error.message);
});
}
public deactivateSelectedProjects(): void {
const promises = this.selection.selected.map(project => {
this.projectService.DeactivateProject(project.id);
});
Promise.all(promises).then(() => {
this.toast.showInfo('Deactivated selected projects Successfully');
}).catch(error => {
this.toast.showInfo(error.message);
});
}
}