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
@@ -4,30 +4,17 @@
<a [routerLink]="[ '/projects' ]" mat-icon-button>
<mat-icon class="icon">arrow_back</mat-icon>
</a>
<h1>{{ 'PROJECT.PAGES.TITLE' | translate }} {{project?.name}}</h1>
<h1>{{ 'PROJECT.PAGES.TITLE' | translate }} {{project?.projectName}}</h1>
<div class="full-width">
<p class="desc">{{ 'PROJECT.PAGES.DESCRIPTION' | translate }}</p>
</div>
</div>
<!-- <ng-template appHasRole [appHasRole]="['project.app.read:' + project?.id, 'project.app.read']">
<app-project-application-grid *ngIf="grid" [disabled]="project?.state !== ProjectState.PROJECTSTATE_ACTIVE"
(changeView)="grid = false" [projectId]="projectId"></app-project-application-grid>
<app-card *ngIf="!grid" title="{{ 'PROJECT.APP.TITLE' | translate }}">
<card-actions class="card-actions">
<button mat-icon-button (click)="grid = true">
<i matTooltip="show grid view" class="las la-th-large"></i>
</button>
</card-actions>
<app-project-applications [disabled]="project?.state !== ProjectState.PROJECTSTATE_ACTIVE"
[projectId]="projectId"></app-project-applications>
</app-card>
</ng-template> -->
<app-card>
<app-project-grant-members *ngIf="project && project.id && project.grantId"
[disabled]="project?.state !== ProjectState.PROJECTSTATE_ACTIVE" [project]="project">
<app-project-grant-members *ngIf="project && project.projectId && project.id"
[disabled]="project?.state !== ProjectState.PROJECTSTATE_ACTIVE || (isZitadel$ | async)"
[project]="project">
</app-project-grant-members>
</app-card>
</div>
@@ -9,7 +9,7 @@ import { ChangeType } from 'src/app/modules/changes/changes.component';
import {
Application,
ApplicationSearchResponse,
GrantedProject,
ProjectGrantView,
ProjectMember,
ProjectMemberSearchResponse,
ProjectRole,
@@ -29,7 +29,7 @@ import { ToastService } from 'src/app/services/toast.service';
export class GrantedProjectDetailComponent implements OnInit, OnDestroy {
public projectId: string = '';
public grantId: string = '';
public project!: GrantedProject.AsObject;
public project!: ProjectGrantView.AsObject;
public pageSizeRoles: number = 10;
public roleDataSource: MatTableDataSource<ProjectRole.AsObject> = new MatTableDataSource<ProjectRole.AsObject>();
@@ -82,7 +82,7 @@ export class GrantedProjectDetailComponent implements OnInit, OnDestroy {
this.grantId = grantId;
if (this.projectId && this.grantId) {
this.projectService.GetGrantedProjectGrantByID(this.projectId, this.grantId).then(proj => {
this.projectService.GetGrantedProjectByID(this.projectId, this.grantId).then(proj => {
this.project = proj.toObject();
console.log(this.project);
this.isZitadel$ = from(this.projectService.SearchApplications(this.project.id, 100, 0).then(appsResp => {
@@ -0,0 +1,58 @@
<div class="view-toggle">
<div class="anim-list" @list *ngIf="selection.selected.length > 0">
<!-- <ng-template appHasRole [appHasRole]="['project.write']">
<button (click)="deactivateProjects(selection.selected)" @animate
matTooltip="{{'PROJECT.TABLE.DEACTIVATE' | translate}}" class="left-button" mat-icon-button>
<i class="las la-toggle-off"></i>
</button>
<button @animate (click)="reactivateProjects(selection.selected)" class="left-button"
matTooltip="{{'PROJECT.TABLE.ACTIVATE' | translate}}" mat-icon-button>
<i class="las la-toggle-on"></i>
</button>
</ng-template> -->
</div>
<button [disabled]="selection.selected.length > 0" (click)="changedView.emit(true)" mat-icon-button>
<i class="show list view las la-th-list"></i>
</button>
</div>
<div class="container">
<mat-progress-bar *ngIf="loading" class="spinner" color="accent" mode="indeterminate"></mat-progress-bar>
<div class="item card" *ngFor="let item of items; index as i" (click)="selectItem(item, $event)"
[ngClass]="{ selected: selection.isSelected(item), inactive: item.state !== ProjectState.PROJECTSTATE_ACTIVE}">
<mat-icon matTooltip="select item" (click)="selection.toggle(item)" class="selection-icon">
check_circle</mat-icon>
<div class="text-part">
<span *ngIf="item.changeDate" class="top">last modified on
{{
dateFromTimestamp(item.changeDate) | date: 'EEE dd. MMM, HH:mm'
}}</span>
<span class="name" *ngIf="item.projectName">{{ item.projectName }}</span>
<span class="description" *ngIf="item.state">{{'PROJECT.STATE.'+item.state | translate}}</span>
<span class="description">{{'PROJECT.TYPE.TITLE' | translate}}:
{{'PROJECT.TYPE.'+ProjectType.PROJECTTYPE_GRANTED | translate}}</span>
<span *ngIf="item.changeDate" class="created">created on
{{
dateFromTimestamp(item.creationDate) | date: 'EEE dd. MMM, HH:mm'
}}</span>
<span class="fill-space"></span>
<div class="icons">
<mat-icon class="icon">apps</mat-icon>
</div>
</div>
<button [matMenuTriggerFor]="editMenu" class="edit-button" mat-icon-button>
<mat-icon>more_vert</mat-icon>
</button>
<mat-menu #editMenu="matMenu">
<ng-template matMenuContent>
<button (click)="selectItem(item)" mat-menu-item>
{{'ACTIONS.VIEW' | translate}}
</button>
<button (click)="selection.toggle(item)" mat-menu-item>
{{'ACTIONS.INFO' | translate}}
</button>
</ng-template>
</mat-menu>
</div>
<p class="n-items" *ngIf="!loading && items.length === 0">{{'PROJECT.PAGES.NOITEMS' | translate}}</p>
</div>
@@ -0,0 +1,219 @@
.view-toggle {
width: 100%;
display: flex;
justify-content: flex-end;
padding-bottom: .5rem;
border-bottom: 1px solid #2d2e30;
.anim-list {
display: flex;
justify-content: flex-end;
align-items: center;
}
button {
&.left-button {
margin-right: 1rem;
}
}
}
.spinner {
margin: 1rem;
}
.container {
display: flex;
flex-wrap: wrap;
margin: 0 -1rem;
box-sizing: border-box;
h2 {
padding: 0 1rem;
flex-basis: 100%;
}
.item {
position: relative;
z-index: 100;
margin: 1rem;
flex-basis: 250px;
display: flex;
text-decoration: none;
cursor: pointer;
padding-top: 0;
padding-right: 0;
padding-bottom: 0;
padding-left: 1rem;
border-radius: 0.5rem;
box-sizing: border-box;
min-height: 166px;
&.inactive {
color: #81868a;
}
.selection-icon {
opacity: 0;
position: absolute;
top: -12px;
left: -12px;
user-select: none;
&:hover {
color: white;
}
}
img {
height: 50px;
width: 50px;
margin: 1rem;
}
.text-part {
flex: 1;
display: flex;
flex-direction: column;
min-height: 70px;
padding: 0.5rem 0;
.top {
font-size: 0.8rem;
margin-bottom: 0;
margin-top: .5rem;
color: #81868a;
}
.name {
margin-top: 1rem;
font-size: 1.2rem;
margin-bottom: 0.5rem;
font-family: 'Rubik', sans-serif;
}
.description {
font-size: 0.8rem;
}
.created {
font-size: 0.8rem;
color: #81868a;
}
.organization {
display: flex;
align-items: center;
margin-top: 1rem;
.org_avatar {
height: 25px;
width: 25px;
border-radius: 50%;
margin: 0;
margin-right: 1rem;
}
}
.fill-space {
flex: 1;
}
.icons {
margin-top: 1rem;
transition: all 0.3s;
opacity: 0;
.icon {
margin-right: 3px;
font-size: 1.3rem;
height: 1.4rem;
color: #81868a;
}
}
span {
margin: 2px 0;
}
}
.edit-button {
position: absolute;
bottom: 0;
right: 0;
margin: 0;
margin-bottom: 0.25rem;
}
&:hover {
.selection-icon {
opacity: 1;
}
.text-part {
.icons {
opacity: 1;
}
}
}
&.selected {
.text-part {
.icons {
opacity: 1;
}
}
.selection-icon {
opacity: 1;
}
.icon {
opacity: 1;
}
}
}
.add-project-button {
z-index: 100;
flex-basis: 250px;
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
min-height: 166px;
border-radius: 0.5rem;
margin: 1rem;
box-sizing: border-box;
font-family: 'Rubik', sans-serif;
.icon {
display: flex;
justify-content: center;
align-self: center;
margin-bottom: 1rem;
height: 2.5rem;
line-height: 2.5rem;
font-size: 2.5rem;
}
&:hover {
background-color: #ffffff25;
.icon,
span {
&.disabled {
color: gray;
}
}
}
}
}
.n-items {
flex-basis: 100%;
padding: 0 1rem;
font-size: .8rem;
color: #81868a;
}
@@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { GrantedProjectGridComponent } from './granted-project-grid.component';
describe('GridComponent', () => {
let component: GrantedProjectGridComponent;
let fixture: ComponentFixture<GrantedProjectGridComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [GrantedProjectGridComponent],
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(GrantedProjectGridComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,65 @@
import { animate, animateChild, query, stagger, style, transition, trigger } from '@angular/animations';
import { SelectionModel } from '@angular/cdk/collections';
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { Router } from '@angular/router';
import { Timestamp } from 'google-protobuf/google/protobuf/timestamp_pb';
import { ProjectGrantView, ProjectState, ProjectType, ProjectView } 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-granted-project-grid',
templateUrl: './granted-project-grid.component.html',
styleUrls: ['./granted-project-grid.component.scss'],
animations: [
trigger('list', [
transition(':enter', [
query('@animate',
stagger(100, 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 GrantedProjectGridComponent {
@Input() items: Array<ProjectGrantView.AsObject> = [];
@Output() newClicked: EventEmitter<boolean> = new EventEmitter();
@Output() changedView: EventEmitter<boolean> = new EventEmitter();
@Input() loading: boolean = false;
public selection: SelectionModel<ProjectView.AsObject> = new SelectionModel<ProjectView.AsObject>(true, []);
public selectedIndex: number = -1;
public showNewProject: boolean = false;
public ProjectState: any = ProjectState;
public ProjectType: any = ProjectType;
constructor(private router: Router, private projectService: ProjectService, private toast: ToastService) { }
public selectItem(item: ProjectGrantView.AsObject, event?: any): void {
if (event && !event.target.classList.contains('mat-icon')) {
this.router.navigate(['projects', item.projectId, 'grant', `${item.id}`]);
} else if (!event) {
this.router.navigate(['projects', item.projectId, 'grant', `${item.id}`]);
}
}
public addItem(): void {
this.newClicked.emit(true);
}
public dateFromTimestamp(date: Timestamp.AsObject): any {
const ts: Date = new Date(date.seconds * 1000 + date.nanos / 1000);
return ts;
}
}
@@ -0,0 +1,112 @@
<app-granted-project-grid *ngIf="grid" [loading]="loading$ | async" (changedView)="grid = false"
[items]="grantedProjectList" (newClicked)="addProject()">
</app-granted-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 && grantedProjectList">
<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) || (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>
@@ -0,0 +1,98 @@
.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;
}
@@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { GrantedProjectListComponent } from './granted-project-list.component';
describe('ProjectListComponent', () => {
let component: GrantedProjectListComponent;
let fixture: ComponentFixture<GrantedProjectListComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [GrantedProjectListComponent],
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(GrantedProjectListComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -3,18 +3,18 @@ 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 { 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 { ProjectGrantView } 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'],
selector: 'app-granted-project-list',
templateUrl: './granted-project-list.component.html',
styleUrls: ['./granted-project-list.component.scss'],
animations: [
trigger('list', [
transition(':enter', [
@@ -35,12 +35,15 @@ import { ToastService } from 'src/app/services/toast.service';
]),
],
})
export class ProjectListComponent implements OnInit, OnDestroy {
export class GrantedProjectListComponent implements OnInit, OnDestroy {
public totalResult: number = 0;
public dataSource: MatTableDataSource<GrantedProject.AsObject> = new MatTableDataSource<GrantedProject.AsObject>();
public projectList: GrantedProject.AsObject[] = [];
public dataSource: MatTableDataSource<ProjectGrantView.AsObject> =
new MatTableDataSource<ProjectGrantView.AsObject>();
public grantedProjectList: ProjectGrantView.AsObject[] = [];
public displayedColumns: string[] = ['select', 'name', 'orgName', 'orgDomain', 'type', 'state', 'creationDate', 'changeDate'];
public selection: SelectionModel<Project.AsObject> = new SelectionModel<Project.AsObject>(true, []);
public selection: SelectionModel<ProjectGrantView.AsObject> = new SelectionModel<ProjectGrantView.AsObject>(true, []);
private loadingSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
public loading$: Observable<boolean> = this.loadingSubject.asObservable();
@@ -49,13 +52,12 @@ export class ProjectListComponent implements OnInit, OnDestroy {
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));
this.getData(10, 0);
}
public ngOnDestroy(): void {
@@ -83,14 +85,13 @@ export class ProjectListComponent implements OnInit, OnDestroy {
}
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.grantedProjectList = res.toObject().resultList;
this.totalResult = res.toObject().totalResult;
this.dataSource.data = this.projectList;
this.dataSource.data = this.grantedProjectList;
this.loadingSubject.next(false);
console.log(this.projectList);
console.log(this.grantedProjectList);
}).catch(error => {
console.error(error);
this.toast.showError(error.message);
@@ -5,15 +5,10 @@
<div class="img-list">
<ng-container *ngIf="totalResult < 10; else compact">
<ng-container *ngFor="let member of membersSubject | async">
<!-- <img (click)="showDetail()" *ngIf="member.imageURL; else render" class="avatar-img"
[src]="member.imageURL" matTooltip="{{ member.email }} | {{member.rolesList?.join(' ')}}"
alt="editor avatar" />
<ng-template #render> -->
<div (click)="showDetail()" class="avatar-circle"
matTooltip="{{ member.email }} | {{member.rolesList?.join(' ')}}">
<mat-icon>face</mat-icon>
<i class="avatar las la-user-circle"></i>
</div>
<!-- </ng-template> -->
</ng-container>
</ng-container>
<ng-template #compact>
@@ -1,20 +1,20 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ProjectContributorsComponent } from './project-contributors.component';
import { OwnedProjectContributorsComponent } from './owned-project-contributors.component';
describe('ProjectContributorsComponent', () => {
let component: ProjectContributorsComponent;
let fixture: ComponentFixture<ProjectContributorsComponent>;
let component: OwnedProjectContributorsComponent;
let fixture: ComponentFixture<OwnedProjectContributorsComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ProjectContributorsComponent],
declarations: [OwnedProjectContributorsComponent],
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ProjectContributorsComponent);
fixture = TestBed.createComponent(OwnedProjectContributorsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
@@ -5,27 +5,27 @@ import { BehaviorSubject, from, of } from 'rxjs';
import { catchError, finalize, map } from 'rxjs/operators';
import { User } from 'src/app/proto/generated/auth_pb';
import {
GrantedProject,
ProjectMemberSearchResponse,
ProjectMemberView,
ProjectState,
ProjectType,
ProjectView,
} from 'src/app/proto/generated/management_pb';
import { ProjectService } from 'src/app/services/project.service';
import { ToastService } from 'src/app/services/toast.service';
import {
CreationType,
ProjectMemberCreateDialogComponent,
} from '../../../modules/add-member-dialog/project-member-create-dialog.component';
MemberCreateDialogComponent,
} from '../../../modules/add-member-dialog/member-create-dialog.component';
@Component({
selector: 'app-project-contributors',
templateUrl: './project-contributors.component.html',
styleUrls: ['./project-contributors.component.scss'],
selector: 'app-owned-project-contributors',
templateUrl: './owned-project-contributors.component.html',
styleUrls: ['./owned-project-contributors.component.scss'],
})
export class ProjectContributorsComponent implements OnInit {
@Input() public project!: GrantedProject.AsObject;
export class OwnedProjectContributorsComponent implements OnInit {
@Input() public project!: ProjectView.AsObject;
@Input() public projectType!: ProjectType;
@Input() public disabled: boolean = false;
@@ -45,9 +45,10 @@ export class ProjectContributorsComponent implements OnInit {
console.log('project grant members');
const promise: Promise<ProjectMemberSearchResponse> | undefined =
this.projectType === ProjectType.PROJECTTYPE_OWNED ?
this.projectService.SearchProjectMembers(this.project.id, 100, 0) :
this.projectService.SearchProjectMembers(this.project.projectId, 100, 0) :
this.projectType === ProjectType.PROJECTTYPE_GRANTED ?
this.projectService.SearchProjectGrantMembers(this.project.id, this.project.grantId, 100, 0) : undefined;
this.projectService.SearchProjectGrantMembers(this.project.projectId,
this.project.projectId, 100, 0) : undefined;
if (promise) {
from(promise).pipe(
map(resp => {
@@ -64,12 +65,10 @@ export class ProjectContributorsComponent implements OnInit {
}
public openAddMember(): void {
const dialogRef = this.dialog.open(ProjectMemberCreateDialogComponent, {
const dialogRef = this.dialog.open(MemberCreateDialogComponent, {
data: {
creationType: this.project.type ===
ProjectType.PROJECTTYPE_GRANTED ? CreationType.PROJECT_GRANTED :
ProjectType.PROJECTTYPE_OWNED ? CreationType.PROJECT_OWNED : undefined,
projectId: this.project.id,
creationType: CreationType.PROJECT_OWNED,
projectId: this.project.projectId,
},
width: '400px',
});
@@ -81,7 +80,7 @@ export class ProjectContributorsComponent implements OnInit {
if (users && users.length && roles && roles.length) {
Promise.all(users.map(user => {
return this.projectService.AddProjectMember(this.project.id, user.id, roles);
return this.projectService.AddProjectMember(this.project.projectId, user.id, roles);
})).then(() => {
this.toast.showError('members added');
}).catch(error => {
@@ -94,7 +93,7 @@ export class ProjectContributorsComponent implements OnInit {
public showDetail(): void {
if (this.project?.state === ProjectState.PROJECTSTATE_ACTIVE) {
this.router.navigate(['projects', this.project.id, 'members']);
this.router.navigate(['projects', this.project.projectId, 'members']);
}
}
}
@@ -6,11 +6,11 @@
</a>
<h1>{{ 'PROJECT.PAGES.TITLE' | translate }} {{project?.name}}</h1>
<ng-template appHasRole [appHasRole]="['project.write:'+projectId, 'project.write']">
<!-- <button *ngIf="project?.type == ProjectType.PROJECTTYPE_OWNED" mat-icon-button
(click)="editstate = !editstate" aria-label="Edit project name">
<button mat-icon-button (click)="editstate = !editstate" aria-label="Edit project name"
*ngIf="(isZitadel$ | async) === false">
<mat-icon *ngIf="!editstate">edit</mat-icon>
<mat-icon *ngIf="editstate">close</mat-icon>
</button> -->
</button>
</ng-template>
<div class="full-width">
@@ -23,10 +23,10 @@
<button class="icon-button" *ngIf="editstate" mat-icon-button (click)="updateName()">
<mat-icon>check</mat-icon>
</button>
<button mat-stroked-button color="accent"
<button mat-stroked-button color="accent" [disabled]="(isZitadel$ | async)"
*ngIf="project?.state === ProjectState.PROJECTSTATE_ACTIVE" class="second"
(click)="changeState(ProjectState.PROJECTSTATE_INACTIVE)">{{'PROJECT.TABLE.DEACTIVATE' | translate}}</button>
<button mat-stroked-button color="accent"
<button mat-stroked-button color="accent" [disabled]="(isZitadel$ | async)"
*ngIf="project?.state === ProjectState.PROJECTSTATE_INACTIVE" class="second"
(click)="changeState(ProjectState.PROJECTSTATE_ACTIVE)">{{'PROJECT.TABLE.ACTIVATE' | translate}}</button>
</ng-container>
@@ -38,15 +38,16 @@
<ng-container *ngIf="project">
<ng-template appHasRole [appHasRole]="['project.app.read:' + project.id, 'project.app.read']">
<app-project-application-grid *ngIf="grid"
[disabled]="project?.state !== ProjectState.PROJECTSTATE_ACTIVE" (changeView)="grid = false"
[projectId]="projectId"></app-project-application-grid>
[disabled]="project?.state !== ProjectState.PROJECTSTATE_ACTIVE || (isZitadel$ | async)"
(changeView)="grid = false" [projectId]="projectId"></app-project-application-grid>
<app-card *ngIf="!grid" title="{{ 'PROJECT.APP.TITLE' | translate }}">
<card-actions class="card-actions">
<button mat-icon-button (click)="grid = true">
<i matTooltip="show grid view" class="las la-th-large"></i>
</button>
</card-actions>
<app-project-applications [disabled]="project?.state !== ProjectState.PROJECTSTATE_ACTIVE"
<app-project-applications
[disabled]="project?.state !== ProjectState.PROJECTSTATE_ACTIVE || (isZitadel$ | async)"
[projectId]="projectId"></app-project-applications>
</app-card>
</ng-template>
@@ -87,10 +88,10 @@
<mat-tab-group mat-stretch-tabs class="tab-group" disablePagination="true">
<mat-tab label="Details">
<app-project-contributors *ngIf="project"
<app-owned-project-contributors *ngIf="project"
[disabled]="project?.state !== ProjectState.PROJECTSTATE_ACTIVE"
[projectType]="ProjectType.PROJECTTYPE_OWNED" [project]="project">
</app-project-contributors>
</app-owned-project-contributors>
</mat-tab>
<mat-tab label="{{ 'CHANGES.PROJECT.TITLE' | translate }}" class="flex-col">
<app-changes *ngIf="project" [changeType]="ChangeType.PROJECT" [id]="project.id"></app-changes>
@@ -0,0 +1,26 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { OwnedProjectDetailComponent } from './owned-project-detail.component';
describe('ProjectDetailComponent', () => {
let component: OwnedProjectDetailComponent;
let fixture: ComponentFixture<OwnedProjectDetailComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [OwnedProjectDetailComponent],
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(OwnedProjectDetailComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -9,7 +9,6 @@ import { ChangeType } from 'src/app/modules/changes/changes.component';
import {
Application,
ApplicationSearchResponse,
GrantedProject,
Project,
ProjectMember,
ProjectMemberSearchResponse,
@@ -23,14 +22,14 @@ import { ProjectService } from 'src/app/services/project.service';
import { ToastService } from 'src/app/services/toast.service';
@Component({
selector: 'app-project-detail',
templateUrl: './project-detail.component.html',
styleUrls: ['./project-detail.component.scss'],
selector: 'app-owned-project-detail',
templateUrl: './owned-project-detail.component.html',
styleUrls: ['./owned-project-detail.component.scss'],
})
export class ProjectDetailComponent implements OnInit, OnDestroy {
export class OwnedProjectDetailComponent implements OnInit, OnDestroy {
public projectId: string = '';
public project!: Project.AsObject | GrantedProject.AsObject;
public project!: Project.AsObject;
public pageSizeRoles: number = 10;
public roleDataSource: MatTableDataSource<ProjectRole.AsObject> = new MatTableDataSource<ProjectRole.AsObject>();
@@ -84,6 +83,7 @@ export class ProjectDetailComponent implements OnInit, OnDestroy {
if (this.projectId) {
this.projectService.GetProjectById(id).then(proj => {
this.project = proj.toObject();
console.log(this.project);
this.isZitadel$ = from(this.projectService.SearchApplications(this.project.id, 100, 0).then(appsResp => {
const ret = appsResp.toObject().resultList
.filter(app => app.oidcConfig?.clientId === this.grpcService.clientid).length > 0;
@@ -29,8 +29,8 @@
}}</span>
<span class="name" *ngIf="item.name">{{ item.name }}</span>
<span class="description" *ngIf="item.state">{{'PROJECT.STATE.'+item.state | translate}}</span>
<span class="description" *ngIf="item.type !== undefined">{{'PROJECT.TYPE.TITLE' | translate}}:
{{'PROJECT.TYPE.'+item.type | translate}}</span>
<span class="description">{{'PROJECT.TYPE.TITLE' | translate}}:
{{'PROJECT.TYPE.'+ProjectType.PROJECTTYPE_OWNED | translate}}</span>
<span *ngIf="item.changeDate" class="created">created on
{{
dateFromTimestamp(item.creationDate) | date: 'EEE dd. MMM, HH:mm'
@@ -55,6 +55,9 @@
</ng-template>
</mat-menu>
</div>
<p class="n-items" *ngIf="!loading && items.length === 0">{{'PROJECT.PAGES.NOITEMS' | translate}}</p>
<div class="add-project-button card" (click)="addItem()">
<mat-icon class="icon">add</mat-icon>
<span>Add new project</span>
@@ -27,6 +27,11 @@
margin: 0 -1rem;
box-sizing: border-box;
h2 {
padding: 0 1rem;
flex-basis: 100%;
}
.item {
position: relative;
z-index: 100;
@@ -204,4 +209,11 @@
}
}
}
}
.n-items {
padding: 0 1rem;
font-size: .8rem;
color: #81868a;
flex-basis: 100%;
}
@@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { OwnedProjectGridComponent } from './owned-project-grid.component';
describe('GridComponent', () => {
let component: OwnedProjectGridComponent;
let fixture: ComponentFixture<OwnedProjectGridComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [OwnedProjectGridComponent],
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(OwnedProjectGridComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -3,14 +3,14 @@ import { SelectionModel } from '@angular/cdk/collections';
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { Router } from '@angular/router';
import { Timestamp } from 'google-protobuf/google/protobuf/timestamp_pb';
import { GrantedProject, Project, ProjectState } from 'src/app/proto/generated/management_pb';
import { ProjectState, ProjectType, ProjectView } 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-grid',
templateUrl: './project-grid.component.html',
styleUrls: ['./project-grid.component.scss'],
selector: 'app-owned-project-grid',
templateUrl: './owned-project-grid.component.html',
styleUrls: ['./owned-project-grid.component.scss'],
animations: [
trigger('list', [
transition(':enter', [
@@ -31,33 +31,26 @@ import { ToastService } from 'src/app/services/toast.service';
]),
],
})
export class ProjectGridComponent {
@Input() items: Array<GrantedProject.AsObject> = [];
export class OwnedProjectGridComponent {
@Input() items: Array<ProjectView.AsObject> = [];
@Output() newClicked: EventEmitter<boolean> = new EventEmitter();
@Output() changedView: EventEmitter<boolean> = new EventEmitter();
@Input() loading: boolean = false;
public selection: SelectionModel<GrantedProject.AsObject> = new SelectionModel<GrantedProject.AsObject>(true, []);
public selection: SelectionModel<ProjectView.AsObject> = new SelectionModel<ProjectView.AsObject>(true, []);
public selectedIndex: number = -1;
public showNewProject: boolean = false;
public ProjectState: any = ProjectState;
public ProjectType: any = ProjectType;
constructor(private router: Router, private projectService: ProjectService, private toast: ToastService) { }
public selectItem(item: GrantedProject.AsObject, event?: any): void {
public selectItem(item: ProjectView.AsObject, event?: any): void {
if (event && !event.target.classList.contains('mat-icon')) {
if (item.grantId) {
this.router.navigate(['projects', item.id, 'grant', `${item.grantId}`]);
} else {
this.router.navigate(['/projects', item.id]);
}
this.router.navigate(['/projects', item.projectId]);
} else if (!event) {
if (item.grantId) {
this.router.navigate(['projects', item.id, 'grant', `${item.grantId}`]);
} else {
this.router.navigate(['/projects', item.id]);
}
this.router.navigate(['/projects', item.projectId]);
}
}
@@ -70,9 +63,9 @@ export class ProjectGridComponent {
return ts;
}
public reactivateProjects(selected: Project.AsObject[]): void {
public reactivateProjects(selected: ProjectView.AsObject[]): void {
Promise.all([selected.map(proj => {
return this.projectService.ReactivateProject(proj.id);
return this.projectService.ReactivateProject(proj.projectId);
})]).then(() => {
this.toast.showInfo('Successful reactivated all projects');
}).catch(error => {
@@ -80,9 +73,9 @@ export class ProjectGridComponent {
});
}
public deactivateProjects(selected: Project.AsObject[]): void {
public deactivateProjects(selected: ProjectView.AsObject[]): void {
Promise.all([selected.map(proj => {
return this.projectService.DeactivateProject(proj.id);
return this.projectService.DeactivateProject(proj.projectId);
})]).then(() => {
this.toast.showInfo('Successful deactivated all projects');
}).catch(error => {
@@ -0,0 +1,111 @@
<app-owned-project-grid *ngIf="grid" [loading]="loading$ | async" (changedView)="grid = false"
[items]="ownedProjectList" (newClicked)="addProject()">
</app-owned-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 && ownedProjectList">
<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) || (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>
@@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { GrantedProjectListComponent } from './granted-project-list.component';
describe('ProjectListComponent', () => {
let component: GrantedProjectListComponent;
let fixture: ComponentFixture<GrantedProjectListComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [GrantedProjectListComponent],
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(GrantedProjectListComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,135 @@
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 { 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 { ProjectView } 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-owned-project-list',
templateUrl: './owned-project-list.component.html',
styleUrls: ['./owned-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 OwnedProjectListComponent implements OnInit, OnDestroy {
public totalResult: number = 0;
public dataSource: MatTableDataSource<ProjectView.AsObject> =
new MatTableDataSource<ProjectView.AsObject>();
public ownedProjectList: ProjectView.AsObject[] = [];
public displayedColumns: string[] = ['select', 'name', 'orgName', 'orgDomain', 'type', 'state', 'creationDate', 'changeDate'];
public selection: SelectionModel<ProjectView.AsObject> = new SelectionModel<ProjectView.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 projectService: ProjectService,
private toast: ToastService,
) { }
public ngOnInit(): void {
console.log('asdf');
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.SearchProjects(limit, offset).then(res => {
this.ownedProjectList = res.toObject().resultList;
this.totalResult = res.toObject().totalResult;
this.dataSource.data = this.ownedProjectList;
this.loadingSubject.next(false);
console.log(this.ownedProjectList);
}).catch(error => {
console.error(error);
this.toast.showError(error.message);
this.loadingSubject.next(false);
});
this.ownedProjectList = [];
}
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.projectId);
});
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.projectId);
});
Promise.all(promises).then(() => {
this.toast.showInfo('Deactivated selected projects Successfully');
}).catch(error => {
this.toast.showInfo(error.message);
});
}
}
@@ -1,25 +0,0 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ProjectDetailComponent } from './project-detail.component';
describe('ProjectDetailComponent', () => {
let component: ProjectDetailComponent;
let fixture: ComponentFixture<ProjectDetailComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ProjectDetailComponent],
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ProjectDetailComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -10,8 +10,8 @@ import { ToastService } from 'src/app/services/toast.service';
import {
CreationType,
ProjectMemberCreateDialogComponent,
} from '../../../modules/add-member-dialog/project-member-create-dialog.component';
MemberCreateDialogComponent,
} from '../../../modules/add-member-dialog/member-create-dialog.component';
import { ProjectGrantMembersDataSource } from './project-grant-members-datasource';
@Component({
@@ -23,7 +23,7 @@ export class ProjectGrantMembersComponent implements AfterViewInit, OnInit {
@Input() public projectId!: string;
@Input() public grantId!: string;
public disabled: boolean = false;
@Input() public disabled: boolean = false;
@ViewChild(MatPaginator) public paginator!: MatPaginator;
@ViewChild(MatTable) public table!: MatTable<ProjectMember.AsObject>;
public dataSource!: ProjectGrantMembersDataSource;
@@ -91,7 +91,7 @@ export class ProjectGrantMembersComponent implements AfterViewInit, OnInit {
}
public openAddMember(): void {
const dialogRef = this.dialog.open(ProjectMemberCreateDialogComponent, {
const dialogRef = this.dialog.open(MemberCreateDialogComponent, {
data: {
creationType: CreationType.PROJECT_GRANTED,
projectId: this.projectId,
@@ -1,25 +0,0 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ProjectGridComponent } from './project-grid.component';
describe('GridComponent', () => {
let component: ProjectGridComponent;
let fixture: ComponentFixture<ProjectGridComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ProjectGridComponent],
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ProjectGridComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -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,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();
});
});
@@ -2,13 +2,13 @@ import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { GrantedProjectDetailComponent } from './granted-project-detail/granted-project-detail.component';
import { ProjectDetailComponent } from './project-detail/project-detail.component';
import { ProjectListComponent } from './project-list/project-list.component';
import { OwnedProjectDetailComponent } from './owned-project-detail/owned-project-detail.component';
import { ProjectsComponent } from './projects.component';
const routes: Routes = [
{
path: '',
component: ProjectListComponent,
component: ProjectsComponent,
data: { animation: 'HomePage' },
},
{
@@ -22,7 +22,7 @@ const routes: Routes = [
},
{
path: ':id',
component: ProjectDetailComponent,
component: OwnedProjectDetailComponent,
data: { animation: 'HomePage' },
},
{
@@ -0,0 +1,11 @@
<div class="max-width-container">
<h1>{{ 'PROJECT.PAGES.LIST' | translate }}</h1>
<p class="sub">{{ 'PROJECT.PAGES.LISTDESCRIPTION' | translate }}</p>
<h2>Owned Projects</h2>
<app-owned-project-list></app-owned-project-list>
<h2>Granted Projects</h2>
<app-granted-project-list>
</app-granted-project-list>
</div>
@@ -0,0 +1,13 @@
h1 {
font-family: ailerons;
margin-top: 0;
}
.sub {
color: #81868a;
margin-bottom: 2rem;
}
.max-width-container {
padding-bottom: 3rem;
}
@@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ProjectsComponent } from './projects.component';
describe('ProjectsComponent', () => {
let component: ProjectsComponent;
let fixture: ComponentFixture<ProjectsComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ProjectsComponent],
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ProjectsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,29 @@
import { Component, OnDestroy, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Subscription } from 'rxjs';
@Component({
selector: 'app-projects',
templateUrl: './projects.component.html',
styleUrls: ['./projects.component.scss'],
})
export class ProjectsComponent implements OnInit, OnDestroy {
// public projectId: string = '';
// public grantId: string = '';
private sub: Subscription = new Subscription();
constructor(private route: ActivatedRoute,
) {
// this.route.params.subscribe((params) => {
// this.projectId = params.projectId;
// this.grantId = params.grantId;
// });
}
ngOnInit(): void {
}
public ngOnDestroy(): void {
// this.sub.unsubscribe();
}
}
@@ -28,34 +28,42 @@ import { SearchUserAutocompleteModule } from 'src/app/modules/search-user-autoco
import { ChangesModule } from '../../modules/changes/changes.module';
import { ProjectRolesModule } from '../../modules/project-roles/project-roles.module';
import { OrgMembersModule } from '../orgs/org-members/org-members.module';
import { OrgContributorsModule } from '../orgs/org-contributors/org-contributors.module';
import { UserListModule } from '../user-list/user-list.module';
import { GrantedProjectDetailComponent } from './granted-project-detail/granted-project-detail.component';
import { GrantedProjectGridComponent } from './granted-project-grid/granted-project-grid.component';
import { GrantedProjectListComponent } from './granted-project-list/granted-project-list.component';
import { OwnedProjectContributorsComponent } from './owned-project-contributors/owned-project-contributors.component';
import { OwnedProjectDetailComponent } from './owned-project-detail/owned-project-detail.component';
import { OwnedProjectGridComponent } from './owned-project-grid/owned-project-grid.component';
import { OwnedProjectListComponent } from './owned-project-list/owned-project-list.component';
import { ProjectApplicationGridComponent } from './project-application-grid/project-application-grid.component';
import { ProjectApplicationsComponent } from './project-applications/project-applications.component';
import { ProjectContributorsComponent } from './project-contributors/project-contributors.component';
import { ProjectDetailComponent } from './project-detail/project-detail.component';
import {
ProjectGrantMembersCreateDialogComponent,
} from './project-grant-members-create-dialog/project-grant-members-create-dialog.component';
import { ProjectGrantMembersComponent } from './project-grant-members/project-grant-members.component';
import { ProjectGrantsComponent } from './project-grants/project-grants.component';
import { ProjectGridComponent } from './project-grid/project-grid.component';
import { ProjectListComponent } from './project-list/project-list.component';
import { ProjectsRoutingModule } from './projects-routing.module';
import { GrantedProjectDetailComponent } from './granted-project-detail/granted-project-detail.component';
import { ProjectsComponent } from './projects.component';
@NgModule({
declarations: [
ProjectListComponent,
ProjectDetailComponent,
GrantedProjectListComponent,
GrantedProjectGridComponent,
GrantedProjectDetailComponent,
OwnedProjectListComponent,
OwnedProjectGridComponent,
OwnedProjectDetailComponent,
ProjectApplicationsComponent,
ProjectGridComponent,
ProjectApplicationGridComponent,
ProjectGrantsComponent,
ProjectGrantMembersComponent,
ProjectGrantMembersCreateDialogComponent,
ProjectContributorsComponent,
GrantedProjectDetailComponent,
OwnedProjectContributorsComponent,
ProjectsComponent,
],
imports: [
ProjectsRoutingModule,
@@ -87,7 +95,7 @@ import { GrantedProjectDetailComponent } from './granted-project-detail/granted-
CardModule,
MatTooltipModule,
MatSortModule,
OrgMembersModule,
OrgContributorsModule,
TranslateModule.forChild({
loader: {
provide: TranslateLoader,
@@ -99,7 +107,7 @@ import { GrantedProjectDetailComponent } from './granted-project-detail/granted-
entryComponents: [
ProjectGrantMembersCreateDialogComponent,
],
exports: [ProjectListComponent],
exports: [],
schemas: [CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA],
})
export class ProjectsModule { }