feat(console): granted project detail (#203)

* granted project detail

* support for grantedProjects

* getGrantedProjectGrantByIDs

* fix grant members bindings

* seperate granted from owned project detail

* lint

* dont show apps on granted projects
This commit is contained in:
Max Peintner
2020-06-10 15:52:56 +02:00
committed by GitHub
parent 9965beee9c
commit a35afd2b88
23 changed files with 2404 additions and 1773 deletions
@@ -0,0 +1,49 @@
<app-meta-layout>
<div class="max-width-container">
<div class="head" *ngIf="project?.id">
<a [routerLink]="[ '/projects' ]" mat-icon-button>
<mat-icon class="icon">arrow_back</mat-icon>
</a>
<h1>{{ 'PROJECT.PAGES.TITLE' | translate }} {{project?.name}}</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>
</app-card>
</div>
<metainfo class="side">
<div class="details">
<div class="row">
<span class="first">{{'PROJECT.TYPE.TITLE' | translate}}:</span>
<span class="second">{{'PROJECT.TYPE.'+ ProjectType.PROJECTTYPE_GRANTED | translate}}</span>
</div>
<div class="row">
<span class="first">{{'PROJECT.STATE.TITLE' | translate}}:</span>
<span *ngIf="project && project.state !== undefined"
class="second">{{'PROJECT.STATE.'+project.state | translate}}</span>
</div>
</div>
<app-changes *ngIf="project" [changeType]="ChangeType.PROJECT" [id]="project.id"></app-changes>
</metainfo>
</app-meta-layout>
@@ -0,0 +1,117 @@
.head {
display: flex;
align-items: center;
border-bottom: 1px solid #ffffff20;
flex-wrap: wrap;
margin-bottom: 1rem;
a {
display: block;
}
h1 {
font-size: 1.2rem;
margin: 0 1rem;
margin-left: 2rem;
font-weight: normal;
}
.full-width {
padding-top: 1rem;
width: 100%;
display: block;
.icon-button {
margin-right: 1rem;
}
button {
border-radius: .5rem;
}
.desc {
font-size: .9rem;
color: #81868a;
}
}
}
.content {
display: flex;
flex-direction: row;
flex-wrap: wrap;
margin: 0 -.5rem;
mat-form-field {
flex: 1 1 33%;
margin: 0 .5rem;
}
}
.side {
.details {
margin-bottom: 1rem;
border-bottom: 1px solid #81868a40;
padding-bottom: 1rem;
.row {
display: flex;
margin-bottom: 0.5rem;
align-items: center;
button {
display: none;
visibility: hidden;
}
&:hover {
button {
display: inline-block;
visibility: visible;
mat-icon {
font-size: 1.2rem;
}
}
}
.first {
flex: 1;
font-size: 0.8rem;
margin-right: 0.5rem;
}
.fill-space {
flex: 1;
}
.second {
font-size: 0.8rem;
text-overflow: ellipsis;
overflow: hidden;
margin-left: 1rem;
}
a {
&:hover {
cursor: pointer;
text-decoration: underline;
}
}
}
.side-section {
color: #81868a;
}
}
.mat-tab-label {
min-width: 100px !important;
}
.flex-col {
display: flex;
flex-direction: column;
}
}
@@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { GrantedProjectDetailComponent } from './granted-project-detail.component';
describe('GrantedProjectDetailComponent', () => {
let component: GrantedProjectDetailComponent;
let fixture: ComponentFixture<GrantedProjectDetailComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [GrantedProjectDetailComponent],
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(GrantedProjectDetailComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,104 @@
import { SelectionModel } from '@angular/cdk/collections';
import { Location } from '@angular/common';
import { Component, OnDestroy, OnInit } from '@angular/core';
import { MatTableDataSource } from '@angular/material/table';
import { ActivatedRoute, Params } from '@angular/router';
import { TranslateService } from '@ngx-translate/core';
import { from, Observable, of, Subscription } from 'rxjs';
import { ChangeType } from 'src/app/modules/changes/changes.component';
import {
Application,
ApplicationSearchResponse,
GrantedProject,
ProjectMember,
ProjectMemberSearchResponse,
ProjectRole,
ProjectRoleSearchResponse,
ProjectState,
ProjectType,
} from 'src/app/proto/generated/management_pb';
import { GrpcService } from 'src/app/services/grpc.service';
import { ProjectService } from 'src/app/services/project.service';
import { ToastService } from 'src/app/services/toast.service';
@Component({
selector: 'app-granted-project-detail',
templateUrl: './granted-project-detail.component.html',
styleUrls: ['./granted-project-detail.component.scss'],
})
export class GrantedProjectDetailComponent implements OnInit, OnDestroy {
public projectId: string = '';
public grantId: string = '';
public project!: GrantedProject.AsObject;
public pageSizeRoles: number = 10;
public roleDataSource: MatTableDataSource<ProjectRole.AsObject> = new MatTableDataSource<ProjectRole.AsObject>();
public roleResult!: ProjectRoleSearchResponse.AsObject;
public roleColumns: string[] = ['name', 'displayname', 'group', 'actions'];
public pageSizeMembers: number = 10;
public projectDataSource: MatTableDataSource<ProjectMember.AsObject> = new MatTableDataSource<ProjectMember.AsObject>();
public memberResult!: ProjectMemberSearchResponse.AsObject;
public memberColumns: string[] = ['firstname', 'lastname', 'username', 'email', 'roles'];
public selection: SelectionModel<ProjectMember.AsObject> = new SelectionModel<ProjectMember.AsObject>(true, []);
public pageSizeApps: number = 10;
public appsDataSource: MatTableDataSource<Application.AsObject> = new MatTableDataSource<Application.AsObject>();
public appsResult!: ApplicationSearchResponse.AsObject;
public appsColumns: string[] = ['name'];
public ProjectState: any = ProjectState;
public ProjectType: any = ProjectType;
public ChangeType: any = ChangeType;
public grid: boolean = true;
private subscription?: Subscription;
public editstate: boolean = false;
public isZitadel$: Observable<boolean> = of(false);
private zitadelsub: Subscription = new Subscription();
constructor(
public translate: TranslateService,
private route: ActivatedRoute,
private toast: ToastService,
private projectService: ProjectService,
private _location: Location,
private grpcService: GrpcService,
) {
}
public ngOnInit(): void {
this.subscription = this.route.params.subscribe(params => this.getData(params));
}
public ngOnDestroy(): void {
this.subscription?.unsubscribe();
this.zitadelsub.unsubscribe();
}
private async getData({ id, grantId }: Params): Promise<void> {
this.projectId = id;
this.grantId = grantId;
if (this.projectId && this.grantId) {
this.projectService.GetGrantedProjectGrantByID(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 => {
const ret = appsResp.toObject().resultList
.filter(app => app.oidcConfig?.clientId === this.grpcService.clientid).length > 0;
return ret;
}));
}).catch(error => {
this.toast.showError(error.message);
});
}
this.zitadelsub = this.isZitadel$.subscribe(isZita => console.log(`zitade: ${isZita}`));
}
public navigateBack(): void {
this._location.back();
}
}
@@ -42,7 +42,7 @@ export class ProjectContributorsComponent implements OnInit {
private router: Router) { }
public ngOnInit(): void {
console.log(this.project);
console.log('project grant members');
const promise: Promise<ProjectMemberSearchResponse> | undefined =
this.projectType === ProjectType.PROJECTTYPE_OWNED ?
this.projectService.SearchProjectMembers(this.project.id, 100, 0) :
@@ -35,7 +35,7 @@
</div>
<!-- show only on owned projects-->
<ng-container *ngIf="project && projectType == ProjectType.PROJECTTYPE_OWNED; else granteddetail">
<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"
@@ -71,20 +71,12 @@
</ng-template>
</ng-container>
</ng-container>
<ng-template #granteddetail>
<app-card>
<app-project-grant-members *ngIf="project"
[disabled]="project?.state !== ProjectState.PROJECTSTATE_ACTIVE" [project]="project">
</app-project-grant-members>
</app-card>
</ng-template>
</div>
<metainfo class="side">
<div class="details">
<div class="row">
<span class="first">{{'PROJECT.TYPE.TITLE' | translate}}:</span>
<span class="second" *ngIf="projectType">{{'PROJECT.TYPE.'+ projectType | translate}}</span>
<span class="second">{{'PROJECT.TYPE.'+ ProjectType.PROJECTTYPE_OWNED | translate}}</span>
</div>
<div class="row">
<span class="first">{{'PROJECT.STATE.TITLE' | translate}}:</span>
@@ -93,22 +85,16 @@
</div>
</div>
<ng-container *ngIf="projectType === ProjectType.PROJECTTYPE_OWNED; else grantedmeta">
<mat-tab-group mat-stretch-tabs class="tab-group" disablePagination="true">
<mat-tab label="Details">
<app-project-contributors *ngIf="project"
[disabled]="project?.state !== ProjectState.PROJECTSTATE_ACTIVE" [projectType]="projectType"
[project]="project">
</app-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>
</mat-tab>
</mat-tab-group>
</ng-container>
<ng-template #grantedmeta>
<app-changes *ngIf="project" [changeType]="ChangeType.PROJECT" [id]="project.id"></app-changes>
</ng-template>
<mat-tab-group mat-stretch-tabs class="tab-group" disablePagination="true">
<mat-tab label="Details">
<app-project-contributors *ngIf="project"
[disabled]="project?.state !== ProjectState.PROJECTSTATE_ACTIVE"
[projectType]="ProjectType.PROJECTTYPE_OWNED" [project]="project">
</app-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>
</mat-tab>
</mat-tab-group>
</metainfo>
</app-meta-layout>
@@ -9,6 +9,7 @@ import { ChangeType } from 'src/app/modules/changes/changes.component';
import {
Application,
ApplicationSearchResponse,
GrantedProject,
Project,
ProjectMember,
ProjectMemberSearchResponse,
@@ -29,9 +30,7 @@ import { ToastService } from 'src/app/services/toast.service';
})
export class ProjectDetailComponent implements OnInit, OnDestroy {
public projectId: string = '';
public grantId: string = '';
public project!: Project.AsObject; // ProjectGrant.AsObject;
public projectType: ProjectType = ProjectType.PROJECTTYPE_OWNED;
public project!: Project.AsObject | GrantedProject.AsObject;
public pageSizeRoles: number = 10;
public roleDataSource: MatTableDataSource<ProjectRole.AsObject> = new MatTableDataSource<ProjectRole.AsObject>();
@@ -58,6 +57,7 @@ export class ProjectDetailComponent implements OnInit, OnDestroy {
public editstate: boolean = false;
public isZitadel$: Observable<boolean> = of(false);
private zitadelsub: Subscription = new Subscription();
constructor(
public translate: TranslateService,
@@ -75,43 +75,26 @@ export class ProjectDetailComponent implements OnInit, OnDestroy {
public ngOnDestroy(): void {
this.subscription?.unsubscribe();
this.zitadelsub.unsubscribe();
}
private async getData({ id, grantId }: Params): Promise<void> {
private async getData({ id }: Params): Promise<void> {
this.projectId = id;
this.grantId = grantId;
if (grantId) {
this.projectType = ProjectType.PROJECTTYPE_GRANTED;
// this.projectService.GetGrantedProjectGrantByID(id, this.grantId).then(proj => {
// this.projectGrant = proj.toObject();
// 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;
// return ret;
// })); // TODO: replace with prettier thing
// }).catch(error => {
// this.toast.showError(error.message);
// });
} else {
this.projectType = ProjectType.PROJECTTYPE_OWNED;
if (this.projectId) {
this.projectService.GetProjectById(id).then(proj => {
this.project = proj.toObject();
// if (this.project.type !== ProjectType.PROJECTTYPE_SELF ||
// this.project.state === ProjectState.PROJECTSTATE_INACTIVE ||
// this.project.state === ProjectState.PROJECTSTATE_UNSPECIFIED) {
// }
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;
return ret;
})); // TODO: replace with prettier thing
this.isZitadel$.subscribe(isZita => console.log(`zitade: ${isZita}`));
}));
}).catch(error => {
this.toast.showError(error.message);
});
}
this.zitadelsub = this.isZitadel$.subscribe(isZita => console.log(`zitade: ${isZita}`));
}
public changeState(newState: ProjectState): void {
@@ -122,16 +105,12 @@ export class ProjectDetailComponent implements OnInit, OnDestroy {
this.toast.showError(error.message);
});
} else if (newState === ProjectState.PROJECTSTATE_INACTIVE) {
this.projectService.DeactivateProject(this.projectId).then(() => {
this.toast.showInfo('Deactivated Project');
}).catch(error => {
this.toast.showError(error.message);
});
this.toast.showInfo('You cant update this project.');
}
}
public saveProject(): void {
this.projectService.UpdateProject(this.project).then(() => {
this.projectService.UpdateProject(this.project as Project.AsObject).then(() => {
this.toast.showInfo('Project updated');
}).catch(error => {
this.toast.showInfo(error.message);
@@ -1,7 +1,7 @@
import { DataSource } from '@angular/cdk/collections';
import { BehaviorSubject, from, Observable, of } from 'rxjs';
import { catchError, finalize, map } from 'rxjs/operators';
import { GrantedProject, ProjectMember } from 'src/app/proto/generated/management_pb';
import { ProjectMember } from 'src/app/proto/generated/management_pb';
import { ProjectService } from 'src/app/services/project.service';
/**
@@ -19,21 +19,24 @@ export class ProjectGrantMembersDataSource extends DataSource<ProjectMember.AsOb
super();
}
public loadMembers(project: GrantedProject.AsObject, pageIndex: number, pageSize: number, sortDirection?: string): void {
public loadMembers(projectId: string, grantId: string, pageIndex: number,
pageSize: number, sortDirection?: string): void {
const offset = pageIndex * pageSize;
this.loadingSubject.next(true);
from(this.projectService.SearchProjectGrantMembers(project.id, project.grantId, pageSize, offset)).pipe(
map(resp => {
this.totalResult = resp.toObject().totalResult;
return resp.toObject().resultList;
}),
catchError(() => of([])),
finalize(() => this.loadingSubject.next(false)),
).subscribe(members => {
this.membersSubject.next(members);
});
from(this.projectService.SearchProjectGrantMembers(projectId,
grantId, pageSize, offset)).pipe(
map(resp => {
this.totalResult = resp.toObject().totalResult;
return resp.toObject().resultList;
}),
catchError(() => of([])),
finalize(() => this.loadingSubject.next(false)),
).subscribe(members => {
console.log(members);
this.membersSubject.next(members);
});
}
@@ -1,4 +1,4 @@
<h1>{{project?.name}} {{ 'PROJECT.MEMBER.TITLE' | translate }}</h1>
<h1>{{ 'PROJECT.MEMBER.TITLE' | translate }}</h1>
<p class="desc">{{ 'PROJECT.MEMBER.DESCRIPTION' | translate }}</p>
<div class="table-header-row">
@@ -3,9 +3,8 @@ import { AfterViewInit, Component, Input, OnInit, ViewChild } from '@angular/cor
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 { GrantedProject, ProjectMember, User } from 'src/app/proto/generated/management_pb';
import { ProjectMember, User } from 'src/app/proto/generated/management_pb';
import { ProjectService } from 'src/app/services/project.service';
import { ToastService } from 'src/app/services/toast.service';
@@ -21,7 +20,9 @@ import { ProjectGrantMembersDataSource } from './project-grant-members-datasourc
styleUrls: ['./project-grant-members.component.scss'],
})
export class ProjectGrantMembersComponent implements AfterViewInit, OnInit {
@Input() public project!: GrantedProject.AsObject;
@Input() public projectId!: string;
@Input() public grantId!: string;
public disabled: boolean = false;
@ViewChild(MatPaginator) public paginator!: MatPaginator;
@ViewChild(MatTable) public table!: MatTable<ProjectMember.AsObject>;
@@ -34,12 +35,12 @@ export class ProjectGrantMembersComponent implements AfterViewInit, OnInit {
constructor(private projectService: ProjectService,
private dialog: MatDialog,
private toast: ToastService,
private route: ActivatedRoute) {
) {
}
public ngOnInit(): void {
this.dataSource = new ProjectGrantMembersDataSource(this.projectService);
this.dataSource.loadMembers(this.project, 0, 25, 'asc');
this.dataSource.loadMembers(this.projectId, this.grantId, 0, 25, 'asc');
}
public ngAfterViewInit(): void {
@@ -48,33 +49,33 @@ export class ProjectGrantMembersComponent implements AfterViewInit, OnInit {
tap(() => this.loadMembersPage()),
)
.subscribe();
}
private loadMembersPage(): void {
this.dataSource.loadMembers(
this.project,
this.projectId,
this.grantId,
this.paginator.pageIndex,
this.paginator.pageSize,
);
}
public removeProjectMemberSelection(): void {
Promise.all(this.selection.selected.map(member => {
return this.projectService.RemoveProjectMember(this.project.id, member.userId).then(() => {
this.toast.showInfo('Removed successfully');
}).catch(error => {
this.toast.showError(error.message);
});
}));
// Promise.all(this.selection.selected.map(member => {
// return this.projectService.RemoveProjectMember(this.projectId, this.grantId, member.userId).then(() => {
// this.toast.showInfo('Removed successfully');
// }).catch(error => {
// this.toast.showError(error.message);
// });
// }));
}
public removeMember(member: ProjectMember.AsObject): void {
this.projectService.RemoveProjectMember(this.project.id, member.userId).then(() => {
this.toast.showInfo('Member removed successfully');
}).catch(error => {
this.toast.showError(error.message);
});
// this.projectService.RemoveProjectMember(this.grantedProject.id, member.userId).then(() => {
// this.toast.showInfo('Member removed successfully');
// }).catch(error => {
// this.toast.showError(error.message);
// });
}
public isAllSelected(): boolean {
@@ -93,19 +94,21 @@ export class ProjectGrantMembersComponent implements AfterViewInit, OnInit {
const dialogRef = this.dialog.open(ProjectMemberCreateDialogComponent, {
data: {
creationType: CreationType.PROJECT_GRANTED,
projectId: this.project.id,
projectId: this.projectId,
},
width: '400px',
});
dialogRef.afterClosed().subscribe(resp => {
if (resp) {
console.log(resp);
const users: User.AsObject[] = resp.users;
const roles: string[] = resp.roles;
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.AddProjectGrantMember(this.projectId,
this.grantId, user.id, roles);
})).then(() => {
this.toast.showError('members added');
}).catch(error => {
@@ -48,19 +48,17 @@ export class ProjectGridComponent {
public selectItem(item: GrantedProject.AsObject, event?: any): void {
if (event && !event.target.classList.contains('mat-icon')) {
if (item.grantId) {
this.router.navigate([item.id, '/grant', `${item.grantId}`]);
this.router.navigate(['projects', item.id, 'grant', `${item.grantId}`]);
} else {
this.router.navigate(['/projects', item.id]);
}
} else if (!event) {
if (item.grantId) {
this.router.navigate([item.id, '/grant', `${item.grantId}`]);
this.router.navigate(['projects', item.id, 'grant', `${item.grantId}`]);
} else {
this.router.navigate(['/projects', item.id]);
}
}
}
public addItem(): void {
@@ -1,6 +1,7 @@
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';
@@ -16,7 +17,7 @@ const routes: Routes = [
},
{
path: ':id/grant/:grantId',
component: ProjectDetailComponent,
component: GrantedProjectDetailComponent,
data: { animation: 'HomePage' },
},
{
@@ -42,6 +42,7 @@ import { ProjectGrantsComponent } from './project-grants/project-grants.componen
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';
@NgModule({
declarations: [
@@ -54,6 +55,7 @@ import { ProjectsRoutingModule } from './projects-routing.module';
ProjectGrantMembersComponent,
ProjectGrantMembersCreateDialogComponent,
ProjectContributorsComponent,
GrantedProjectDetailComponent,
],
imports: [
ProjectsRoutingModule,