feat(console): pinned org list, project grant detail view, state change, i18n, domain dialog, policy refactor, theme fixes, refactor user and projects (#449)

* pinned organisations

* project grant detail, state update, user-list pad

* rm entry components

* members nav, i18n, disable actions on non active

* add org domain dialog

* mv password policy rm to detail view

* prefix pinned orgs for userid, fix collapsed pad

* fix app back navigation

* rem pwd required validator

* fix org item overflow

* routing

* move users modules to users page

* reorganize projects

* remove child init of translate

* hide same preferred loginname
This commit is contained in:
Max Peintner
2020-07-13 13:38:49 +02:00
committed by GitHub
parent d8eef34a37
commit b628baeb1a
193 changed files with 967 additions and 500 deletions
@@ -0,0 +1,46 @@
<app-meta-layout>
<div class="max-width-container">
<div class="head" *ngIf="project?.id">
<a [routerLink]="[ '/granted-projects' ]" mat-icon-button>
<mat-icon class="icon">arrow_back</mat-icon>
</a>
<h1>{{ 'PROJECT.PAGES.TITLE' | translate }} {{project?.projectName}}</h1>
<div class="full-width">
<p class="desc">{{ 'PROJECT.PAGES.DESCRIPTION' | translate }}</p>
<p *ngIf="isZitadel" class="zitadel-warning">{{'PROJECT.PAGES.ZITADELPROJECT' | translate}}</p>
</div>
</div>
<!-- <ng-template appHasRole [appHasRole]="['project.grant.user.grant.read']"> -->
<app-card *ngIf="project?.projectId" title="{{ 'GRANTS.PROJECT.TITLE' | translate }}"
description="{{'GRANTS.PROJECT.DESCRIPTION' | translate }}">
<app-user-grants [context]="userGrantContext" [projectId]="projectId" [grantId]="grantId"
[allowCreate]="['project.grant.user.grant.write'] | hasRole"
[allowDelete]="['project.grant.user.grant.delete'] | hasRole">
</app-user-grants>
</app-card>
<!-- </ng-template> -->
</div>
<metainfo class="side">
<div class="details">
<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>
<mat-tab-group mat-stretch-tabs class="tab-group" disablePagination="true">
<mat-tab label="Details">
<app-project-contributors *ngIf="project && grantId"
[disabled]="project?.state !== ProjectState.PROJECTSTATE_ACTIVE"
[projectType]="ProjectType.PROJECTTYPE_GRANTED" [project]="project" [grantId]="grantId">
</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>
@@ -0,0 +1,131 @@
.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: #8795a1;
}
.zitadel-warning {
font-size: 14px;
color: rgb(201,51,71);
}
}
}
.content {
display: flex;
flex-direction: row;
flex-wrap: wrap;
margin: 0 -.5rem;
mat-form-field {
flex: 1 1 33%;
margin: 0 .5rem;
}
}
h1 {
font-size: 1.2rem;
}
.desc {
width: 100%;
display: block;
font-size: .9rem;
color: #8795a1;
}
.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: #8795a1;
}
}
.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 { Subscription } from 'rxjs';
import { ChangeType } from 'src/app/modules/changes/changes.component';
import { UserGrantContext } from 'src/app/modules/user-grants/user-grants-datasource';
import {
Application,
ApplicationSearchResponse,
ProjectGrantView,
ProjectMember,
ProjectMemberSearchResponse,
ProjectRole,
ProjectRoleSearchResponse,
ProjectState,
ProjectType,
UserGrantSearchKey,
} from 'src/app/proto/generated/management_pb';
import { OrgService } from 'src/app/services/org.service';
import { ProjectService } from 'src/app/services/project.service';
import { ToastService } from 'src/app/services/toast.service';
@Component({
selector: 'app-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!: ProjectGrantView.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: boolean = false;
public userGrantContext: UserGrantContext = UserGrantContext.GRANTED_PROJECT;
public userGrantSearchKey: UserGrantSearchKey = UserGrantSearchKey.USERGRANTSEARCHKEY_PROJECT_ID;
constructor(
public translate: TranslateService,
private route: ActivatedRoute,
private toast: ToastService,
private projectService: ProjectService,
private _location: Location,
private orgService: OrgService,
) {
}
public ngOnInit(): void {
this.subscription = this.route.params.subscribe(params => this.getData(params));
}
public ngOnDestroy(): void {
this.subscription?.unsubscribe();
}
private async getData({ id, grantId }: Params): Promise<void> {
this.projectId = id;
this.grantId = grantId;
this.orgService.GetIam().then(iam => {
this.isZitadel = iam.toObject().iamProjectId === this.projectId;
});
if (this.projectId && this.grantId) {
this.projectService.GetGrantedProjectByID(this.projectId, this.grantId).then(proj => {
this.project = proj.toObject();
console.log('granted-project', this.project);
}).catch(error => {
this.toast.showError(error);
});
}
}
public navigateBack(): void {
this._location.back();
}
}
@@ -0,0 +1,60 @@
<div class="view-toggle">
<button (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>
<p class="n-items" *ngIf="!loading && selection.selected.length > 0">{{'PROJECT.PAGES.PINNED' | translate}}</p>
<div class="item card" *ngFor="let item of selection.selected; index as i"
[ngClass]="{ inactive: item.state !== ProjectState.PROJECTSTATE_ACTIVE}"
(click)="navigateToProject(item.projectId,item.id, $event)">
<div class="text-part">
<span *ngIf="item.changeDate" class="top">{{'PROJECT.PAGES.LASTMODIFIED' | translate}}
{{
item.changeDate | timestampToDate | date: 'EEE dd. MMM, HH:mm'
}}</span>
<span class="name" *ngIf="item.projectName">{{ item.projectName }}</span>
<span class="description" *ngIf="item.resourceOwnerName">{{item.resourceOwnerName}}</span>
<span *ngIf="item.changeDate" class="created">{{'PROJECT.PAGES.CREATEDON' | translate}}
{{ item.creationDate | timestampToDate | date: 'EEE dd. MMM, HH:mm' }}</span>
<span class="fill-space"></span>
<div class="icons">
</div>
</div>
<button [ngClass]="{ selected: selection.isSelected(item)}" (click)="selection.toggle(item)" class="edit-button"
mat-icon-button>
<mat-icon>push_pin_outline</mat-icon>
</button>
</div>
</div>
<div class="container">
<p class="n-items" *ngIf="!loading && notPinned.length > 0">{{'PROJECT.PAGES.ALL' | translate}}</p>
<div class="item card" *ngFor="let item of notPinned; index as i"
(click)="navigateToProject(item.projectId,item.id, $event)"
[ngClass]="{ inactive: item.state !== ProjectState.PROJECTSTATE_ACTIVE}">
<div class="text-part">
<span *ngIf="item.changeDate" class="top">{{'PROJECT.PAGES.LASTMODIFIED' | translate}}
{{
item.changeDate | timestampToDate | date: 'EEE dd. MMM, HH:mm'
}}</span>
<span class="name" *ngIf="item.projectName">{{ item.projectName }}</span>
<span class="description" *ngIf="item.resourceOwnerName">{{item.resourceOwnerName}}</span>
<span *ngIf="item.changeDate" class="created">{{'PROJECT.PAGES.CREATEDON' | translate}}
{{ item.creationDate | timestampToDate | date: 'EEE dd. MMM, HH:mm' }}</span>
<span class="fill-space"></span>
<div class="icons">
</div>
</div>
<button [ngClass]="{ selected: selection.isSelected(item)}" (click)="selection.toggle(item)" class="edit-button"
mat-icon-button>
<mat-icon>push_pin_outline</mat-icon>
</button>
</div>
<p class="n-items" *ngIf="!loading && items.length === 0 && selection.selected.length === 0">
{{'PROJECT.PAGES.NOITEMS' | translate}}</p>
</div>
@@ -0,0 +1,215 @@
.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: #8795a1;
}
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: #8795a1;
}
.name {
margin-top: 1rem;
font-size: 1.2rem;
margin-bottom: 0.5rem;
}
.description {
font-size: 0.8rem;
}
.created {
font-size: 0.8rem;
color: #8795a1;
}
.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: #8795a1;
}
}
span {
margin: 2px 0;
}
}
.edit-button {
opacity: 0;
user-select: none;
position: absolute;
bottom: 0;
right: 0;
margin: 0;
margin-bottom: 0.25rem;
color: #8795a1;
&:hover {
color: white;
}
&.selected {
opacity: 1;
}
}
&:hover {
.edit-button {
opacity: 1;
}
.text-part {
.icons {
opacity: 1;
}
}
}
&.selected {
.text-part {
.icons {
opacity: 1;
}
}
.edit-button {
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;
.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: #8795a1;
}
@@ -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,107 @@
import { animate, animateChild, query, stagger, style, transition, trigger } from '@angular/animations';
import { SelectionModel } from '@angular/cdk/collections';
import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core';
import { Router } from '@angular/router';
import { ProjectGrantView, ProjectState, ProjectType } from 'src/app/proto/generated/management_pb';
import { AuthService } from 'src/app/services/auth.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 implements OnChanges {
@Input() items: Array<ProjectGrantView.AsObject> = [];
public notPinned: Array<ProjectGrantView.AsObject> = [];
@Output() newClicked: EventEmitter<boolean> = new EventEmitter();
@Output() changedView: EventEmitter<boolean> = new EventEmitter();
@Input() loading: boolean = false;
public selection: SelectionModel<ProjectGrantView.AsObject> = new SelectionModel<ProjectGrantView.AsObject>(true, []);
public showNewProject: boolean = false;
public ProjectState: any = ProjectState;
public ProjectType: any = ProjectType;
constructor(private authService: AuthService, private router: Router) {
this.selection.changed.subscribe(selection => {
this.setPrefixedItem('pinned-granted-projects', JSON.stringify(
this.selection.selected.map(item => item.projectId),
)).then(() => {
const filtered = this.notPinned.filter(item => item === selection.added.find(i => i === item));
filtered.forEach((f, i) => {
this.notPinned.splice(i, 1);
});
this.notPinned.push(...selection.removed);
});
});
}
public addItem(): void {
this.newClicked.emit(true);
}
public ngOnChanges(changes: SimpleChanges): void {
if (changes.items?.currentValue && changes.items.currentValue.length > 0) {
this.notPinned = Object.assign([], this.items);
this.reorganizeItems();
}
}
public reorganizeItems(): void {
this.getPrefixedItem('pinned-granted-projects').then(storageEntry => {
if (storageEntry) {
const array: string[] = JSON.parse(storageEntry);
const toSelect: ProjectGrantView.AsObject[] = this.items.filter((item, index) => {
if (array.includes(item.projectId)) {
// this.notPinned.splice(index, 1);
return true;
}
});
this.selection.select(...toSelect);
const toNotPinned: ProjectGrantView.AsObject[] = this.items.filter((item, index) => {
if (!array.includes(item.projectId)) {
return true;
}
});
this.notPinned = toNotPinned;
}
});
}
private async getPrefixedItem(key: string): Promise<string | null> {
const prefix = (await this.authService.GetActiveOrg()).id;
return localStorage.getItem(`${prefix}:${key}`);
}
private async setPrefixedItem(key: string, value: any): Promise<void> {
const prefix = (await this.authService.GetActiveOrg()).id;
return localStorage.setItem(`${prefix}:${key}`, value);
}
public navigateToProject(projectId: string, id: string, event: any): void {
if (event && event.srcElement && event.srcElement.localName !== 'button') {
this.router.navigate(['/granted-projects', projectId, 'grant', id]);
}
}
}
@@ -0,0 +1,96 @@
<app-granted-project-grid *ngIf="grid" [loading]="loading$ | async" (changedView)="grid = false"
[items]="grantedProjectList">
</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>
</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 color="primary" (change)="$event ? masterToggle() : null"
[checked]="selection.hasValue() && isAllSelected()"
[indeterminate]="selection.hasValue() && !isAllSelected()">
</mat-checkbox>
</th>
<td class="selection" mat-cell *matCellDef="let row">
<mat-checkbox color="primary" (click)="$event.stopPropagation()"
(change)="$event ? selection.toggle(row) : null" [checked]="selection.isSelected(row)">
</mat-checkbox>
</td>
</ng-container>
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.NAME' | translate }} </th>
<td mat-cell *matCellDef="let project"> {{project.projectName}} </td>
</ng-container>
<ng-container matColumnDef="resourceOwnerName">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.TABLE.RESOURCEOWNER' | translate }} </th>
<td class="pointer" mat-cell *matCellDef="let project">
{{project.resourceOwnerName}} </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="creationDate">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.TABLE.CREATIONDATE' | translate }} </th>
<td mat-cell *matCellDef="let project">
<span
*ngIf="project.creationDate">{{project.creationDate | timestampToDate | 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">{{project.changeDate | timestampToDate | 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]="['/granted-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,94 @@
.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: #8795a1;
}
.count {
font-size: 2rem;
}
}
.fill-space {
flex: 1;
}
.action-btns {
display: flex;
align-items: center;
}
.icon-button {
margin-right: .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();
});
});
@@ -0,0 +1,127 @@
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 { BehaviorSubject, Observable, Subscription } from 'rxjs';
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-granted-project-list',
templateUrl: './granted-project-list.component.html',
styleUrls: ['./granted-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 GrantedProjectListComponent implements OnInit, OnDestroy {
public totalResult: number = 0;
public dataSource: MatTableDataSource<ProjectGrantView.AsObject> =
new MatTableDataSource<ProjectGrantView.AsObject>();
public grantedProjectList: ProjectGrantView.AsObject[] = [];
public displayedColumns: string[] = ['select', 'name', 'resourceOwnerName', 'state', 'creationDate', 'changeDate'];
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();
public grid: boolean = true;
private subscription?: Subscription;
constructor(private router: Router,
public translate: TranslateService,
private projectService: ProjectService,
private toast: ToastService,
) { }
public ngOnInit(): void {
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> {
this.loadingSubject.next(true);
this.projectService.SearchGrantedProjects(limit, offset).then(res => {
this.grantedProjectList = res.toObject().resultList;
this.totalResult = res.toObject().totalResult;
if (this.totalResult > 5) {
this.grid = false;
}
this.dataSource.data = this.grantedProjectList;
this.loadingSubject.next(false);
}).catch(error => {
console.error(error);
this.toast.showError(error);
this.loadingSubject.next(false);
});
}
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);
});
}
}
@@ -0,0 +1,53 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { AuthGuard } from 'src/app/guards/auth.guard';
import { RoleGuard } from 'src/app/guards/role.guard';
import { ProjectType } from 'src/app/proto/generated/management_pb';
import { GrantedProjectDetailComponent } from './granted-project-detail/granted-project-detail.component';
import { GrantedProjectsComponent } from './granted-projects.component';
const routes: Routes = [
{
path: '',
component: GrantedProjectsComponent,
data: { animation: 'HomePage' },
},
{
path: 'create',
loadChildren: () => import('../project-create/project-create.module').then(m => m.ProjectCreateModule),
canActivate: [AuthGuard, RoleGuard],
data: {
roles: ['project.write'],
},
},
{
path: ':projectid/grant/:grantid/members',
data: {
type: ProjectType.PROJECTTYPE_GRANTED,
},
loadChildren: () => import('src/app/modules/project-members/project-members.module')
.then(m => m.ProjectMembersModule),
},
{
path: ':id/grant/:grantId',
component: GrantedProjectDetailComponent,
data: { animation: 'HomePage' },
},
{
path: ':projectid/roles/create',
loadChildren: () => import('../project-role-create/project-role-create.module').then(m => m.ProjectRoleCreateModule),
},
{
path: ':projectid/grants/create',
loadChildren: () => import('../project-grant-create/project-grant-create.module')
.then(m => m.ProjectGrantCreateModule),
},
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class GrantedProjectsRoutingModule { }
@@ -0,0 +1,8 @@
<div class="max-width-container">
<h1>{{ 'PROJECT.PAGES.LIST' | translate }}</h1>
<p class="sub">{{ 'PROJECT.PAGES.LISTDESCRIPTION' | translate }}</p>
<h2>{{'PROJECT.PAGES.TYPE.GRANTED' | translate}}</h2>
<app-granted-project-list>
</app-granted-project-list>
</div>
@@ -0,0 +1,12 @@
h1 {
margin-top: 0;
}
.sub {
color: #8795a1;
margin-bottom: 2rem;
}
.max-width-container {
padding-bottom: 3rem;
}
@@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { GrantedProjectsComponent } from './granted-projects.component';
describe('GrantedProjectsComponent', () => {
let component: GrantedProjectsComponent;
let fixture: ComponentFixture<GrantedProjectsComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [GrantedProjectsComponent],
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(GrantedProjectsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,10 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-granted-projects',
templateUrl: './granted-projects.component.html',
styleUrls: ['./granted-projects.component.scss'],
})
export class GrantedProjectsComponent {
constructor() { }
}
@@ -0,0 +1,71 @@
import { CommonModule } from '@angular/common';
import { NgModule, NO_ERRORS_SCHEMA } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatPaginatorModule } from '@angular/material/paginator';
import { MatProgressBarModule } from '@angular/material/progress-bar';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatSelectModule } from '@angular/material/select';
import { MatSortModule } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table';
import { MatTabsModule } from '@angular/material/tabs';
import { MatTooltipModule } from '@angular/material/tooltip';
import { TranslateModule } from '@ngx-translate/core';
import { HasRoleModule } from 'src/app/directives/has-role/has-role.module';
import { CardModule } from 'src/app/modules/card/card.module';
import { ChangesModule } from 'src/app/modules/changes/changes.module';
import { MetaLayoutModule } from 'src/app/modules/meta-layout/meta-layout.module';
import { ProjectContributorsModule } from 'src/app/modules/project-contributors/project-contributors.module';
import { ProjectRolesModule } from 'src/app/modules/project-roles/project-roles.module';
import { UserGrantsModule } from 'src/app/modules/user-grants/user-grants.module';
import { PipesModule } from 'src/app/pipes/pipes.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 { GrantedProjectsRoutingModule } from './granted-projects-routing.module';
import { GrantedProjectsComponent } from './granted-projects.component';
@NgModule({
declarations: [
GrantedProjectsComponent,
GrantedProjectListComponent,
GrantedProjectGridComponent,
GrantedProjectDetailComponent,
],
imports: [
CommonModule,
UserGrantsModule,
GrantedProjectsRoutingModule,
ProjectContributorsModule,
FormsModule,
TranslateModule,
ReactiveFormsModule,
HasRoleModule,
MatTableModule,
MatPaginatorModule,
MatFormFieldModule,
MatInputModule,
ChangesModule,
MatIconModule,
MatSelectModule,
MatButtonModule,
MatProgressSpinnerModule,
MetaLayoutModule,
MatProgressBarModule,
MatTabsModule,
ProjectRolesModule,
MatCheckboxModule,
CardModule,
MatTooltipModule,
MatSortModule,
PipesModule,
TranslateModule,
],
schemas: [NO_ERRORS_SCHEMA],
})
export class GrantedProjectsModule { }