mirror of
https://github.com/andwati/zitadel.git
synced 2026-11-04 13:22:16 +00:00
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:
+115
@@ -0,0 +1,115 @@
|
||||
<app-meta-layout>
|
||||
<div class="max-width-container">
|
||||
<div class="head" *ngIf="project?.projectId">
|
||||
<a [routerLink]="[ '/projects' ]" mat-icon-button>
|
||||
<mat-icon class="icon">arrow_back</mat-icon>
|
||||
</a>
|
||||
<h1>{{ 'PROJECT.PAGES.TITLE' | translate }} {{project?.name}}</h1>
|
||||
<ng-template appHasRole [appHasRole]="['project.write:'+projectId, 'project.write']">
|
||||
<button mat-icon-button (click)="editstate = !editstate" aria-label="Edit project name"
|
||||
*ngIf="isZitadel === false">
|
||||
<mat-icon *ngIf="!editstate">edit</mat-icon>
|
||||
<mat-icon *ngIf="editstate">close</mat-icon>
|
||||
</button>
|
||||
</ng-template>
|
||||
|
||||
<span class="fill-space"></span>
|
||||
|
||||
<button mat-stroked-button color="accent" [disabled]="isZitadel"
|
||||
*ngIf="project?.state === ProjectState.PROJECTSTATE_ACTIVE" class="state-button"
|
||||
(click)="changeState(ProjectState.PROJECTSTATE_INACTIVE)">{{'PROJECT.TABLE.DEACTIVATE' | translate}}</button>
|
||||
<button mat-stroked-button color="accent" [disabled]="isZitadel"
|
||||
*ngIf="project?.state === ProjectState.PROJECTSTATE_INACTIVE" class="state-button"
|
||||
(click)="changeState(ProjectState.PROJECTSTATE_ACTIVE)">{{'PROJECT.TABLE.ACTIVATE' | translate}}</button>
|
||||
|
||||
<div class="full-width">
|
||||
<div class="line">
|
||||
<ng-container *ngIf="editstate">
|
||||
<mat-form-field *ngIf="editstate && project?.name" class="formfield"
|
||||
hintLabel="The name is required!">
|
||||
<mat-label>{{'PROJECT.NAME' | translate}}</mat-label>
|
||||
<input matInput [(ngModel)]="project.name" />
|
||||
</mat-form-field>
|
||||
<button class="icon-button" *ngIf="editstate" mat-icon-button (click)="updateName()">
|
||||
<mat-icon>check</mat-icon>
|
||||
</button>
|
||||
</ng-container>
|
||||
<span class="fill-space"></span>
|
||||
</div>
|
||||
<p class="desc">{{ 'PROJECT.PAGES.DESCRIPTION' | translate }}</p>
|
||||
<p *ngIf="isZitadel" class="zitadel-warning">{{'PROJECT.PAGES.ZITADELPROJECT' | translate}}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ng-container *ngIf="project">
|
||||
<ng-template appHasRole [appHasRole]="['project.app.read:' + project.projectId, 'project.app.read']">
|
||||
<app-project-application-grid *ngIf="grid"
|
||||
[disabled]="project?.state !== ProjectState.PROJECTSTATE_ACTIVE || isZitadel"
|
||||
(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 || isZitadel"
|
||||
[projectId]="projectId"></app-project-applications>
|
||||
</app-card>
|
||||
</ng-template>
|
||||
|
||||
<ng-container *ngIf="isZitadel == false">
|
||||
<ng-template appHasRole
|
||||
[appHasRole]="['project.grant.read:' + project.projectId, 'project.grant.read']">
|
||||
<app-card title="{{ 'PROJECT.GRANT.TITLE' | translate }}"
|
||||
description="{{ 'PROJECT.GRANT.DESCRIPTION' | translate }}">
|
||||
<app-project-grants [disabled]="project?.state !== ProjectState.PROJECTSTATE_ACTIVE"
|
||||
[projectId]="projectId">
|
||||
</app-project-grants>
|
||||
</app-card>
|
||||
</ng-template>
|
||||
|
||||
<ng-template appHasRole [appHasRole]="['project.role.read:' + project.projectId, 'project.role.read']">
|
||||
<app-card title="{{ 'PROJECT.ROLE.TITLE' | translate }}"
|
||||
description="{{ 'PROJECT.ROLE.DESCRIPTION' | translate }}">
|
||||
<app-project-roles [disabled]="project?.state !== ProjectState.PROJECTSTATE_ACTIVE"
|
||||
[actionsVisible]="true" [projectId]="projectId">
|
||||
</app-project-roles>
|
||||
</app-card>
|
||||
</ng-template>
|
||||
|
||||
<ng-template appHasRole [appHasRole]="['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"
|
||||
[disabled]="project?.state !== ProjectState.PROJECTSTATE_ACTIVE"
|
||||
[allowCreate]="project?.state == ProjectState.PROJECTSTATE_ACTIVE && (['user.grant.write'] | hasRole)"
|
||||
[allowDelete]="project?.state == ProjectState.PROJECTSTATE_ACTIVE && (['user.grant.delete'] | hasRole)">
|
||||
</app-user-grants>
|
||||
</app-card>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</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"
|
||||
[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.projectId"></app-changes>
|
||||
</mat-tab>
|
||||
</mat-tab-group>
|
||||
</metainfo>
|
||||
</app-meta-layout>
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
.head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid #ffffff20;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 1rem;
|
||||
|
||||
.fill-space {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.state-button {
|
||||
border-radius: .5rem;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
+26
@@ -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();
|
||||
});
|
||||
});
|
||||
+164
@@ -0,0 +1,164 @@
|
||||
import { SelectionModel } from '@angular/cdk/collections';
|
||||
import { Location } from '@angular/common';
|
||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
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 { WarnDialogComponent } from 'src/app/modules/warn-dialog/warn-dialog.component';
|
||||
import {
|
||||
Application,
|
||||
ApplicationSearchResponse,
|
||||
ProjectMember,
|
||||
ProjectMemberSearchResponse,
|
||||
ProjectRole,
|
||||
ProjectRoleSearchResponse,
|
||||
ProjectState,
|
||||
ProjectType,
|
||||
ProjectView,
|
||||
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-owned-project-detail',
|
||||
templateUrl: './owned-project-detail.component.html',
|
||||
styleUrls: ['./owned-project-detail.component.scss'],
|
||||
|
||||
})
|
||||
export class OwnedProjectDetailComponent implements OnInit, OnDestroy {
|
||||
public projectId: string = '';
|
||||
public project!: ProjectView.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 userGrantSearchKey: UserGrantSearchKey = UserGrantSearchKey.USERGRANTSEARCHKEY_PROJECT_ID;
|
||||
public userGrantContext: UserGrantContext = UserGrantContext.OWNED_PROJECT;
|
||||
|
||||
constructor(
|
||||
public translate: TranslateService,
|
||||
private route: ActivatedRoute,
|
||||
private toast: ToastService,
|
||||
private projectService: ProjectService,
|
||||
private _location: Location,
|
||||
private orgService: OrgService,
|
||||
private dialog: MatDialog,
|
||||
) {
|
||||
}
|
||||
|
||||
public ngOnInit(): void {
|
||||
this.subscription = this.route.params.subscribe(params => this.getData(params));
|
||||
}
|
||||
|
||||
public ngOnDestroy(): void {
|
||||
this.subscription?.unsubscribe();
|
||||
}
|
||||
|
||||
private async getData({ id }: Params): Promise<void> {
|
||||
this.projectId = id;
|
||||
|
||||
this.orgService.GetIam().then(iam => {
|
||||
this.isZitadel = iam.toObject().iamProjectId === this.projectId;
|
||||
});
|
||||
|
||||
if (this.projectId) {
|
||||
this.projectService.GetProjectById(id).then(proj => {
|
||||
this.project = proj.toObject();
|
||||
}).catch(error => {
|
||||
console.error(error);
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public changeState(newState: ProjectState): void {
|
||||
if (newState === ProjectState.PROJECTSTATE_ACTIVE) {
|
||||
const dialogRef = this.dialog.open(WarnDialogComponent, {
|
||||
data: {
|
||||
confirmKey: 'ACTIONS.REACTIVATE',
|
||||
cancelKey: 'ACTIONS.CANCEL',
|
||||
titleKey: 'PROJECT.PAGES.DIALOG.REACTIVATE.TITLE',
|
||||
descriptionKey: 'PROJECT.PAGES.DIALOG.REACTIVATE.DESCRIPTION',
|
||||
},
|
||||
width: '400px',
|
||||
});
|
||||
dialogRef.afterClosed().subscribe(resp => {
|
||||
if (resp) {
|
||||
this.projectService.ReactivateProject(this.projectId).then(() => {
|
||||
this.toast.showInfo('Reactivated Project');
|
||||
this.project.state = ProjectState.PROJECTSTATE_ACTIVE;
|
||||
}).catch(error => {
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
} else if (newState === ProjectState.PROJECTSTATE_INACTIVE) {
|
||||
const dialogRef = this.dialog.open(WarnDialogComponent, {
|
||||
data: {
|
||||
confirmKey: 'ACTIONS.DEACTIVATE',
|
||||
cancelKey: 'ACTIONS.CANCEL',
|
||||
titleKey: 'PROJECT.PAGES.DIALOG.DEACTIVATE.TITLE',
|
||||
descriptionKey: 'PROJECT.PAGES.DIALOG.DEACTIVATE.DESCRIPTION',
|
||||
},
|
||||
width: '400px',
|
||||
});
|
||||
dialogRef.afterClosed().subscribe(resp => {
|
||||
if (resp) {
|
||||
this.projectService.DeactivateProject(this.projectId).then(() => {
|
||||
this.toast.showInfo('Deactivated Project');
|
||||
this.project.state = ProjectState.PROJECTSTATE_INACTIVE;
|
||||
}).catch(error => {
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public saveProject(): void {
|
||||
this.projectService.UpdateProject(this.project.projectId, this.project.name).then(() => {
|
||||
this.toast.showInfo('Project updated');
|
||||
}).catch(error => {
|
||||
this.toast.showInfo(error.message);
|
||||
});
|
||||
}
|
||||
|
||||
public navigateBack(): void {
|
||||
this._location.back();
|
||||
}
|
||||
|
||||
public updateName(): void {
|
||||
this.saveProject();
|
||||
this.editstate = false;
|
||||
}
|
||||
}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
<div class="view-toggle">
|
||||
<button (click)="closeGridView()" 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"
|
||||
(click)="navigateToProject(item.projectId, $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.name">{{ item.name }}</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, $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.name">{{ item.name }}</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">{{'PROJECT.PAGES.NOITEMS' | translate}}</p>
|
||||
|
||||
<ng-template appHasRole [appHasRole]="['project.write']">
|
||||
<div class="add-project-button card" (click)="addItem()">
|
||||
<mat-icon class="icon">add</mat-icon>
|
||||
<span>{{'PROJECT.PAGES.ADDNEW' | translate}}</span>
|
||||
</div>
|
||||
</ng-template>
|
||||
</div>
|
||||
+216
@@ -0,0 +1,216 @@
|
||||
.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 {
|
||||
padding: 0 1rem;
|
||||
font-size: .8rem;
|
||||
color: #8795a1;
|
||||
flex-basis: 100%;
|
||||
}
|
||||
+25
@@ -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();
|
||||
});
|
||||
});
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
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 { ProjectState, ProjectType, ProjectView } from 'src/app/proto/generated/management_pb';
|
||||
import { AuthService } from 'src/app/services/auth.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-owned-project-grid',
|
||||
templateUrl: './owned-project-grid.component.html',
|
||||
styleUrls: ['./owned-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 OwnedProjectGridComponent implements OnChanges {
|
||||
@Input() items: Array<ProjectView.AsObject> = [];
|
||||
public notPinned: Array<ProjectView.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 showNewProject: boolean = false;
|
||||
public ProjectState: any = ProjectState;
|
||||
public ProjectType: any = ProjectType;
|
||||
|
||||
constructor(private router: Router, private authService: AuthService) {
|
||||
this.selection.changed.subscribe(selection => {
|
||||
this.setPrefixedItem('pinned-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 selectItem(item: ProjectView.AsObject, event?: any): void {
|
||||
if (event && !event.target.classList.contains('mat-icon')) {
|
||||
this.router.navigate(['/projects', item.projectId]);
|
||||
} else if (!event) {
|
||||
this.router.navigate(['/projects', item.projectId]);
|
||||
}
|
||||
}
|
||||
|
||||
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-projects').then(storageEntry => {
|
||||
if (storageEntry) {
|
||||
const array: string[] = JSON.parse(storageEntry);
|
||||
const toSelect: ProjectView.AsObject[] = this.items.filter((item, index) => {
|
||||
if (array.includes(item.projectId)) {
|
||||
// this.notPinned.splice(index, 1);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
this.selection.select(...toSelect);
|
||||
|
||||
const toNotPinned: ProjectView.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(id: string, event: any): void {
|
||||
if (event && event.srcElement && event.srcElement.localName !== 'button') {
|
||||
this.router.navigate(['/projects', id]);
|
||||
}
|
||||
}
|
||||
|
||||
public closeGridView(): void {
|
||||
this.changedView.emit(true);
|
||||
}
|
||||
}
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
<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>
|
||||
<ng-template appHasRole [appHasRole]="['project.write']">
|
||||
<a class="add-button" [routerLink]="[ '/projects', 'create']" color="primary" mat-raised-button>
|
||||
<mat-icon class="icon">add</mat-icon>{{ 'ACTIONS.NEW' | translate }}
|
||||
</a>
|
||||
</ng-template>
|
||||
</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.name}} </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]="['/projects', row.projectId]"></tr>
|
||||
|
||||
</table>
|
||||
<mat-paginator class="background-style" [length]="totalResult" [pageSize]="10" [pageSizeOptions]="[5, 10, 20]"
|
||||
(page)="changePage($event)"></mat-paginator>
|
||||
</div>
|
||||
</div>
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
h1 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.sub {
|
||||
color: #8795a1;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
margin: 0 -.5rem;
|
||||
|
||||
mat-form-field {
|
||||
flex: 1 1 33%;
|
||||
margin: 0 .5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.view-toggle {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
||||
button {
|
||||
display: block;
|
||||
border-radius: .5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.table-header-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.col {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.desc {
|
||||
font-size: .8rem;
|
||||
color: #8795a1;
|
||||
}
|
||||
.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;
|
||||
}
|
||||
+25
@@ -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();
|
||||
});
|
||||
});
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
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 { 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', '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 {
|
||||
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.SearchProjects(limit, offset).then(res => {
|
||||
this.ownedProjectList = res.toObject().resultList;
|
||||
this.totalResult = res.toObject().totalResult;
|
||||
if (this.totalResult > 5) {
|
||||
this.grid = false;
|
||||
}
|
||||
this.dataSource.data = this.ownedProjectList;
|
||||
this.loadingSubject.next(false);
|
||||
}).catch(error => {
|
||||
console.error(error);
|
||||
this.toast.showError(error);
|
||||
this.loadingSubject.next(false);
|
||||
});
|
||||
|
||||
this.ownedProjectList = [];
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
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 { OwnedProjectDetailComponent } from './owned-project-detail/owned-project-detail.component';
|
||||
import { OwnedProjectsComponent } from './owned-projects.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: OwnedProjectsComponent,
|
||||
data: { animation: 'HomePage' },
|
||||
},
|
||||
{
|
||||
path: 'create',
|
||||
loadChildren: () => import('../project-create/project-create.module').then(m => m.ProjectCreateModule),
|
||||
canActivate: [AuthGuard, RoleGuard],
|
||||
data: {
|
||||
roles: ['project.write'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: ':id',
|
||||
component: OwnedProjectDetailComponent,
|
||||
data: { animation: 'HomePage' },
|
||||
},
|
||||
{
|
||||
path: ':projectid/members',
|
||||
data: {
|
||||
type: ProjectType.PROJECTTYPE_OWNED,
|
||||
},
|
||||
loadChildren: () => import('src/app/modules/project-members/project-members.module')
|
||||
.then(m => m.ProjectMembersModule),
|
||||
},
|
||||
{
|
||||
path: ':projectid/apps',
|
||||
data: { animation: 'AddPage' },
|
||||
loadChildren: () => import('src/app/pages/projects/apps/apps.module').then(m => m.AppsModule),
|
||||
},
|
||||
{
|
||||
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),
|
||||
},
|
||||
{
|
||||
path: ':projectid/grant/:grantid',
|
||||
loadChildren: () => import('./project-grant-detail/project-grant-detail.module')
|
||||
.then(m => m.ProjectGrantDetailModule),
|
||||
},
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class OwnedProjectsRoutingModule { }
|
||||
@@ -0,0 +1,7 @@
|
||||
<div class="max-width-container">
|
||||
<h1>{{ 'PROJECT.PAGES.LIST' | translate }}</h1>
|
||||
<p class="sub">{{ 'PROJECT.PAGES.LISTDESCRIPTION' | translate }}</p>
|
||||
|
||||
<h2>{{'PROJECT.PAGES.TYPE.OWNED' | translate}}</h2>
|
||||
<app-owned-project-list></app-owned-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 { OwnedProjectsComponent } from './owned-projects.component';
|
||||
|
||||
describe('OwnedProjectComponent', () => {
|
||||
let component: OwnedProjectsComponent;
|
||||
let fixture: ComponentFixture<OwnedProjectsComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [OwnedProjectsComponent],
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(OwnedProjectsComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-owned-projects',
|
||||
templateUrl: './owned-projects.component.html',
|
||||
styleUrls: ['./owned-projects.component.scss'],
|
||||
})
|
||||
export class OwnedProjectsComponent {
|
||||
constructor() { }
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
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 { MatChipsModule } from '@angular/material/chips';
|
||||
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 { AvatarModule } from 'src/app/modules/avatar/avatar.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 { WarnDialogModule } from 'src/app/modules/warn-dialog/warn-dialog.module';
|
||||
import { PipesModule } from 'src/app/pipes/pipes.module';
|
||||
|
||||
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 { OwnedProjectsRoutingModule } from './owned-projects-routing.module';
|
||||
import { OwnedProjectsComponent } from './owned-projects.component';
|
||||
import { ProjectApplicationGridComponent } from './project-application-grid/project-application-grid.component';
|
||||
import { ProjectApplicationsComponent } from './project-applications/project-applications.component';
|
||||
import { ProjectGrantsComponent } from './project-grants/project-grants.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
OwnedProjectsComponent,
|
||||
OwnedProjectListComponent,
|
||||
OwnedProjectGridComponent,
|
||||
OwnedProjectDetailComponent,
|
||||
ProjectApplicationGridComponent,
|
||||
ProjectApplicationsComponent,
|
||||
ProjectGrantsComponent,
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
OwnedProjectsRoutingModule,
|
||||
UserGrantsModule,
|
||||
ProjectContributorsModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
TranslateModule,
|
||||
AvatarModule,
|
||||
ReactiveFormsModule,
|
||||
HasRoleModule,
|
||||
MatTableModule,
|
||||
MatPaginatorModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
ChangesModule,
|
||||
MatChipsModule,
|
||||
MatIconModule,
|
||||
MatButtonModule,
|
||||
WarnDialogModule,
|
||||
MatProgressSpinnerModule,
|
||||
MetaLayoutModule,
|
||||
MatProgressBarModule,
|
||||
ProjectRolesModule,
|
||||
MatTabsModule,
|
||||
MatCheckboxModule,
|
||||
CardModule,
|
||||
MatSelectModule,
|
||||
MatTooltipModule,
|
||||
MatSortModule,
|
||||
PipesModule,
|
||||
TranslateModule,
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA],
|
||||
})
|
||||
export class OwnedProjectsModule { }
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
<div class="header">
|
||||
<h3>{{'APP.LIST' | translate}}</h3>
|
||||
<span class="fill-space"></span>
|
||||
<button mat-icon-button (click)="closeView()">
|
||||
<i class="show list view las la-th-list"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="app-container">
|
||||
<div [routerLink]="['/projects', projectId, 'apps', app.id ]" class="app-wrap"
|
||||
*ngFor="let app of appsSubject | async">
|
||||
<div class="morph-card">
|
||||
{{ app.name.charAt(0)}}
|
||||
</div>
|
||||
<span class="name">{{app.name}}</span>
|
||||
</div>
|
||||
<ng-template appHasRole [appHasRole]="['project.app.write']">
|
||||
<div class="app-wrap" *ngIf="!disabled" [routerLink]="[ '/projects', projectId, 'apps', 'create']">
|
||||
<div class="morph-card add">
|
||||
<mat-icon>add</mat-icon>
|
||||
</div>
|
||||
<span class="name">{{ 'ACTIONS.NEW' | translate }}</span>
|
||||
</div>
|
||||
</ng-template>
|
||||
</div>
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.fill-space {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.app-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin: 0 -1rem;
|
||||
padding-bottom: 2rem;
|
||||
|
||||
.app-wrap {
|
||||
outline: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
max-width: 150px;
|
||||
|
||||
.morph-card {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-size: .8rem;
|
||||
color: #8a868a;
|
||||
}
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ProjectApplicationGridComponent } from './project-application-grid.component';
|
||||
|
||||
describe('AppGridComponent', () => {
|
||||
let component: ProjectApplicationGridComponent;
|
||||
let fixture: ComponentFixture<ProjectApplicationGridComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ProjectApplicationGridComponent],
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ProjectApplicationGridComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
||||
import { BehaviorSubject, from, Observable, of } from 'rxjs';
|
||||
import { catchError, finalize, map } from 'rxjs/operators';
|
||||
import { Application } from 'src/app/proto/generated/management_pb';
|
||||
import { ProjectService } from 'src/app/services/project.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-project-application-grid',
|
||||
templateUrl: './project-application-grid.component.html',
|
||||
styleUrls: ['./project-application-grid.component.scss'],
|
||||
})
|
||||
export class ProjectApplicationGridComponent implements OnInit {
|
||||
@Input() public projectId: string = '';
|
||||
@Input() public disabled: boolean = false;
|
||||
@Output() public changeView: EventEmitter<void> = new EventEmitter();
|
||||
public appsSubject: BehaviorSubject<Application.AsObject[]> = new BehaviorSubject<Application.AsObject[]>([]);
|
||||
private loadingSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
|
||||
public loading$: Observable<boolean> = this.loadingSubject.asObservable();
|
||||
|
||||
constructor(private projectService: ProjectService) { }
|
||||
|
||||
public ngOnInit(): void {
|
||||
this.loadApps();
|
||||
}
|
||||
|
||||
public loadApps(): void {
|
||||
from(this.projectService.SearchApplications(this.projectId, 100, 0)).pipe(
|
||||
map(resp => {
|
||||
return resp.toObject().resultList;
|
||||
}),
|
||||
catchError(() => of([])),
|
||||
finalize(() => this.loadingSubject.next(false)),
|
||||
).subscribe((apps) => {
|
||||
this.appsSubject.next(apps as Application.AsObject[]);
|
||||
});
|
||||
}
|
||||
|
||||
public closeView(): void {
|
||||
this.changeView.emit();
|
||||
}
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
import { DataSource } from '@angular/cdk/collections';
|
||||
import { BehaviorSubject, from, Observable, of } from 'rxjs';
|
||||
import { catchError, finalize, map } from 'rxjs/operators';
|
||||
import { Application } from 'src/app/proto/generated/management_pb';
|
||||
import { ProjectService } from 'src/app/services/project.service';
|
||||
|
||||
/**
|
||||
* Data source for the ProjectMembers view. This class should
|
||||
* encapsulate all logic for fetching and manipulating the displayed data
|
||||
* (including sorting, pagination, and filtering).
|
||||
*/
|
||||
export class ProjectApplicationsDataSource extends DataSource<Application.AsObject> {
|
||||
public totalResult: number = 0;
|
||||
public appsSubject: BehaviorSubject<Application.AsObject[]> = new BehaviorSubject<Application.AsObject[]>([]);
|
||||
private loadingSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
|
||||
public loading$: Observable<boolean> = this.loadingSubject.asObservable();
|
||||
|
||||
constructor(private projectService: ProjectService) {
|
||||
super();
|
||||
}
|
||||
|
||||
public loadApps(projectId: string, pageIndex: number, pageSize: number, sortDirection?: string): void {
|
||||
const offset = pageIndex * pageSize;
|
||||
|
||||
this.loadingSubject.next(true);
|
||||
from(this.projectService.SearchApplications(projectId, pageSize, offset)).pipe(
|
||||
map(resp => {
|
||||
this.totalResult = resp.toObject().totalResult;
|
||||
return resp.toObject().resultList;
|
||||
}),
|
||||
catchError(() => of([])),
|
||||
finalize(() => this.loadingSubject.next(false)),
|
||||
).subscribe(apps => {
|
||||
this.appsSubject.next(apps);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Connect this data source to the table. The table will only update when
|
||||
* the returned stream emits new items.
|
||||
* @returns A stream of the items to be rendered.
|
||||
*/
|
||||
public connect(): Observable<Application.AsObject[]> {
|
||||
return this.appsSubject.asObservable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the table is being destroyed. Use this function, to clean up
|
||||
* any open connections or free any held resources that were set up during connect.
|
||||
*/
|
||||
public disconnect(): void {
|
||||
this.appsSubject.complete();
|
||||
this.loadingSubject.complete();
|
||||
}
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
<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?.appsSubject.value.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>
|
||||
<!-- <button [disabled]="disabled" matTooltip="{{'ORG_DETAIL.TABLE.DELETE' | translate}}" class="icon-button"
|
||||
mat-icon-button *ngIf="selection.hasValue()">
|
||||
<i class="las la-trash"></i>
|
||||
</button> -->
|
||||
<ng-template appHasRole [appHasRole]="['project.app.write']">
|
||||
<a [disabled]="disabled" class="add-button" [routerLink]="[ '/projects', projectId, 'apps', 'create']"
|
||||
color="primary" mat-raised-button>
|
||||
<mat-icon class="icon">add</mat-icon>{{ 'ACTIONS.NEW' | translate }}
|
||||
</a>
|
||||
</ng-template>
|
||||
</div>
|
||||
|
||||
<div class="table-wrapper">
|
||||
<div class="spinner-container" *ngIf="dataSource.loading$ | async">
|
||||
<mat-spinner diameter="50"></mat-spinner>
|
||||
</div>
|
||||
|
||||
<table [dataSource]="dataSource" mat-table class="full-width-table" matSort aria-label="Elements">
|
||||
<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.ROLE.NAME' | translate }} </th>
|
||||
<td class="pointer" [routerLink]="['/projects', projectId, 'apps', role.id ]" mat-cell
|
||||
*matCellDef="let role">
|
||||
{{role.name}} </td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr class="data-row" mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
||||
</table>
|
||||
|
||||
<mat-paginator #paginator [length]="dataSource.totalResult" [pageSize]="50" [pageSizeOptions]="[25, 50, 100, 250]">
|
||||
</mat-paginator>
|
||||
</div>
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.margin-neg {
|
||||
margin-left: -1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pointer {
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
tr {
|
||||
outline: none;
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { MatPaginatorModule } from '@angular/material/paginator';
|
||||
import { MatSortModule } from '@angular/material/sort';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
|
||||
import { ProjectApplicationsComponent } from './project-applications.component';
|
||||
|
||||
describe('ProjectApplicationsComponent', () => {
|
||||
let component: ProjectApplicationsComponent;
|
||||
let fixture: ComponentFixture<ProjectApplicationsComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ProjectApplicationsComponent],
|
||||
imports: [
|
||||
NoopAnimationsModule,
|
||||
MatPaginatorModule,
|
||||
MatSortModule,
|
||||
MatTableModule,
|
||||
],
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ProjectApplicationsComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should compile', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
import { SelectionModel } from '@angular/cdk/collections';
|
||||
import { AfterViewInit, Component, Input, OnInit, ViewChild } from '@angular/core';
|
||||
import { MatPaginator } from '@angular/material/paginator';
|
||||
import { MatSort } from '@angular/material/sort';
|
||||
import { MatTable } from '@angular/material/table';
|
||||
import { merge } from 'rxjs';
|
||||
import { tap } from 'rxjs/operators';
|
||||
import { Application } from 'src/app/proto/generated/management_pb';
|
||||
import { ProjectService } from 'src/app/services/project.service';
|
||||
import { ToastService } from 'src/app/services/toast.service';
|
||||
|
||||
import { ProjectApplicationsDataSource } from './project-applications-datasource';
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'app-project-applications',
|
||||
templateUrl: './project-applications.component.html',
|
||||
styleUrls: ['./project-applications.component.scss'],
|
||||
})
|
||||
export class ProjectApplicationsComponent implements AfterViewInit, OnInit {
|
||||
@Input() public projectId: string = '';
|
||||
@Input() public disabled: boolean = false;
|
||||
@ViewChild(MatPaginator) public paginator!: MatPaginator;
|
||||
@ViewChild(MatSort) public sort!: MatSort;
|
||||
@ViewChild(MatTable) public table!: MatTable<Application.AsObject>;
|
||||
public dataSource!: ProjectApplicationsDataSource;
|
||||
public selection: SelectionModel<Application.AsObject> = new SelectionModel<Application.AsObject>(true, []);
|
||||
|
||||
/** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */
|
||||
public displayedColumns: string[] = ['select', 'name'];
|
||||
|
||||
constructor(private projectService: ProjectService, private toast: ToastService) { }
|
||||
|
||||
public ngOnInit(): void {
|
||||
this.dataSource = new ProjectApplicationsDataSource(this.projectService);
|
||||
this.dataSource.loadApps(this.projectId, 0, 25, 'asc');
|
||||
}
|
||||
|
||||
public ngAfterViewInit(): void {
|
||||
this.sort.sortChange.subscribe(() => this.paginator.pageIndex = 0);
|
||||
merge(this.sort.sortChange, this.paginator.page)
|
||||
.pipe(
|
||||
tap(() => this.loadRolesPage()),
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
}
|
||||
|
||||
private loadRolesPage(): void {
|
||||
this.dataSource.loadApps(
|
||||
this.projectId,
|
||||
this.paginator.pageIndex,
|
||||
this.paginator.pageSize,
|
||||
this.sort.direction,
|
||||
);
|
||||
}
|
||||
|
||||
public isAllSelected(): boolean {
|
||||
const numSelected = this.selection.selected.length;
|
||||
const numRows = this.dataSource.appsSubject.value.length;
|
||||
return numSelected === numRows;
|
||||
}
|
||||
|
||||
public masterToggle(): void {
|
||||
this.isAllSelected() ?
|
||||
this.selection.clear() :
|
||||
this.dataSource.appsSubject.value.forEach((row: Application.AsObject) => this.selection.select(row));
|
||||
}
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
import { DataSource } from '@angular/cdk/collections';
|
||||
import { BehaviorSubject, from, Observable, of } from 'rxjs';
|
||||
import { catchError, finalize, map } from 'rxjs/operators';
|
||||
import { ProjectMemberView } from 'src/app/proto/generated/management_pb';
|
||||
import { ProjectService } from 'src/app/services/project.service';
|
||||
|
||||
/**
|
||||
* Data source for the ProjectMembers view. This class should
|
||||
* encapsulate all logic for fetching and manipulating the displayed data
|
||||
* (including sorting, pagination, and filtering).
|
||||
*/
|
||||
export class ProjectGrantDetailDataSource extends DataSource<ProjectMemberView.AsObject> {
|
||||
public totalResult: number = 0;
|
||||
public membersSubject: BehaviorSubject<ProjectMemberView.AsObject[]>
|
||||
= new BehaviorSubject<ProjectMemberView.AsObject[]>([]);
|
||||
private loadingSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
|
||||
public loading$: Observable<boolean> = this.loadingSubject.asObservable();
|
||||
|
||||
constructor(private projectService: ProjectService) {
|
||||
super();
|
||||
}
|
||||
|
||||
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(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 => {
|
||||
this.membersSubject.next(members);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Connect this data source to the table. The table will only update when
|
||||
* the returned stream emits new items.
|
||||
* @returns A stream of the items to be rendered.
|
||||
*/
|
||||
public connect(): Observable<ProjectMemberView.AsObject[]> {
|
||||
return this.membersSubject.asObservable();
|
||||
};
|
||||
|
||||
/**
|
||||
* Called when the table is being destroyed. Use this function, to clean up
|
||||
* any open connections or free any held resources that were set up during connect.
|
||||
*/
|
||||
public disconnect(): void {
|
||||
this.membersSubject.complete();
|
||||
this.loadingSubject.complete();
|
||||
}
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
|
||||
import { ProjectGrantDetailComponent } from './project-grant-detail.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: ProjectGrantDetailComponent,
|
||||
data: { animation: 'AddPage' },
|
||||
},
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class ProjectGrantDetailRoutingModule { }
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
<div class="max-width-container">
|
||||
<div class="container">
|
||||
<div class="left">
|
||||
<a *ngIf="projectid" [routerLink]="[ '/projects', projectid]" mat-icon-button>
|
||||
<mat-icon class="icon">arrow_back</mat-icon>
|
||||
</a>
|
||||
</div>
|
||||
<div class="right">
|
||||
<div class="head">
|
||||
<h1>{{ 'PROJECT.GRANT.DETAIL.TITLE' | translate }}</h1>
|
||||
<p class="desc">{{ 'PROJECT.GRANT.DETAIL.DESC' | translate }}</p>
|
||||
</div>
|
||||
<div class="master-row">
|
||||
<div class="left-col">
|
||||
<div class="row">
|
||||
<span class="first">{{'PROJECT.GRANT.DETAIL.PROJECTNAME' | translate}}</span>
|
||||
<span class="fill-space"></span>
|
||||
<span>{{grant?.projectName}}</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span class="first">{{'PROJECT.GRANT.DETAIL.RESOURCEOWNER' | translate}}</span>
|
||||
<span class="fill-space"></span>
|
||||
<span>{{grant?.resourceOwnerName}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span class="fill-space"></span>
|
||||
|
||||
<div>
|
||||
<button mat-stroked-button color="accent"
|
||||
*ngIf="grant?.state === ProjectGrantState.PROJECTGRANTSTATE_ACTIVE" class="state-button"
|
||||
(click)="changeState(ProjectGrantState.PROJECTGRANTSTATE_INACTIVE)">{{'USER.PAGES.DEACTIVATE' | translate}}</button>
|
||||
<button mat-stroked-button color="accent"
|
||||
*ngIf="grant?.state === ProjectGrantState.PROJECTGRANTSTATE_INACTIVE" class="state-button"
|
||||
(click)="changeState(ProjectGrantState.PROJECTGRANTSTATE_ACTIVE)">{{'USER.PAGES.REACTIVATE' | translate}}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<mat-form-field class="form-field" appearance="outline" *ngIf="grant && grant.roleKeysList">
|
||||
<mat-label>{{ 'PROJECT.GRANT.ROLENAMESLIST' | translate }}</mat-label>
|
||||
<mat-select [(ngModel)]="grant.roleKeysList" multiple (selectionChange)="updateRoles($event)">
|
||||
<mat-option *ngFor="let role of memberRoleOptions" [value]="role.key">
|
||||
{{role.key}}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
<h1>{{ 'PROJECT.GRANT.DETAIL.MEMBERTITLE' | translate }}</h1>
|
||||
<p class="desc">{{ 'PROJECT.GRANT.DETAIL.MEMBERDESC' | translate }}</p>
|
||||
|
||||
<app-project-grant-members *ngIf="this.projectid && this.grantid" [disabled]="isZitadel"
|
||||
[projectId]="projectid" [grantId]="grantid" [type]="projectType">
|
||||
</app-project-grant-members>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
.container {
|
||||
display: flex;
|
||||
padding-bottom: 3rem;
|
||||
|
||||
h1 {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.desc {
|
||||
display: block;
|
||||
font-size: .9rem;
|
||||
color: #8795a1;
|
||||
}
|
||||
|
||||
.left {
|
||||
width: 100px;
|
||||
display: flex;
|
||||
padding: 1rem;
|
||||
justify-content: center;
|
||||
|
||||
a {
|
||||
margin-top: .2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.right {
|
||||
flex: 1;
|
||||
padding-top: 1rem;
|
||||
|
||||
.head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid #ffffff20;
|
||||
margin-bottom: 2rem;
|
||||
flex-wrap: wrap;
|
||||
|
||||
a {
|
||||
display: block;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin-top: 0.2rem;
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.master-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
width: 100%;
|
||||
|
||||
.left-col {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 2rem;
|
||||
|
||||
.row {
|
||||
max-width: 400px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: .5rem 0;
|
||||
|
||||
span {
|
||||
font-size: .9rem;
|
||||
}
|
||||
|
||||
.first {
|
||||
color: #8795a1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fill-space {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.state-button {
|
||||
border-radius: .5rem;
|
||||
}
|
||||
}
|
||||
|
||||
mat-form-field {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.divider {
|
||||
height: 1px;
|
||||
background-color: #ffffff20;
|
||||
}
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ProjectGrantDetailComponent } from './project-grant-detail.component';
|
||||
|
||||
describe('GrantComponent', () => {
|
||||
let component: ProjectGrantDetailComponent;
|
||||
let fixture: ComponentFixture<ProjectGrantDetailComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ProjectGrantDetailComponent],
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ProjectGrantDetailComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { MatSelectChange } from '@angular/material/select';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import {
|
||||
ProjectGrant,
|
||||
ProjectGrantState,
|
||||
ProjectGrantView,
|
||||
ProjectRoleView,
|
||||
ProjectType,
|
||||
} 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-project-grant-detail',
|
||||
templateUrl: './project-grant-detail.component.html',
|
||||
styleUrls: ['./project-grant-detail.component.scss'],
|
||||
})
|
||||
export class ProjectGrantDetailComponent {
|
||||
public grant!: ProjectGrantView.AsObject;
|
||||
public projectid: string = '';
|
||||
public grantid: string = '';
|
||||
|
||||
public projectType: ProjectType = ProjectType.PROJECTTYPE_OWNED;
|
||||
public disabled: boolean = false;
|
||||
|
||||
public isZitadel: boolean = false;
|
||||
ProjectGrantState: any = ProjectGrantState;
|
||||
|
||||
public memberRoleOptions: ProjectRoleView.AsObject[] = [];
|
||||
|
||||
constructor(
|
||||
private orgService: OrgService,
|
||||
private projectService: ProjectService,
|
||||
private route: ActivatedRoute,
|
||||
private toast: ToastService,
|
||||
) {
|
||||
this.route.params.subscribe(params => {
|
||||
this.projectid = params.projectid;
|
||||
this.grantid = params.grantid;
|
||||
|
||||
this.orgService.GetIam().then(iam => {
|
||||
this.isZitadel = iam.toObject().iamProjectId === this.projectid;
|
||||
});
|
||||
|
||||
this.getRoleOptions(params.projectid);
|
||||
|
||||
this.projectService.ProjectGrantByID(this.grantid, this.projectid).then((grant) => {
|
||||
this.grant = grant.toObject();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public changeState(newState: ProjectGrantState): void {
|
||||
if (newState === ProjectGrantState.PROJECTGRANTSTATE_ACTIVE) {
|
||||
this.projectService.ReactivateProjectGrant(this.grantid, this.projectid).then(() => {
|
||||
this.toast.showInfo('PROJECT.TOAST.REACTIVATED', true);
|
||||
this.grant.state = newState;
|
||||
}).catch(error => {
|
||||
this.toast.showError(error);
|
||||
});
|
||||
} else if (newState === ProjectGrantState.PROJECTGRANTSTATE_INACTIVE) {
|
||||
this.projectService.DeactivateProjectGrant(this.grantid, this.projectid).then(() => {
|
||||
this.toast.showInfo('PROJECT.TOAST.DEACTIVATED', true);
|
||||
this.grant.state = newState;
|
||||
}).catch(error => {
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public getRoleOptions(projectId: string): void {
|
||||
this.projectService.SearchProjectRoles(projectId, 100, 0).then(resp => {
|
||||
this.memberRoleOptions = resp.toObject().resultList;
|
||||
console.log(resp.toObject());
|
||||
});
|
||||
}
|
||||
|
||||
updateRoles(selectionChange: MatSelectChange): void {
|
||||
this.projectService.UpdateProjectGrant(this.grant.id, this.grant.projectId, selectionChange.value)
|
||||
.then((newgrant: ProjectGrant) => {
|
||||
this.toast.showInfo('Grant updated!');
|
||||
}).catch(error => {
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatAutocompleteModule } from '@angular/material/autocomplete';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
import { MatChipsModule } from '@angular/material/chips';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatPaginatorModule } from '@angular/material/paginator';
|
||||
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 { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { HasRoleModule } from 'src/app/directives/has-role/has-role.module';
|
||||
|
||||
import { ProjectGrantDetailRoutingModule } from './project-grant-detail-routing.module';
|
||||
import { ProjectGrantDetailComponent } from './project-grant-detail.component';
|
||||
import { ProjectGrantMembersModule } from './project-grant-members/project-grant-members.module';
|
||||
|
||||
|
||||
@NgModule({
|
||||
declarations: [ProjectGrantDetailComponent],
|
||||
imports: [
|
||||
CommonModule,
|
||||
ProjectGrantDetailRoutingModule,
|
||||
ProjectGrantMembersModule,
|
||||
MatAutocompleteModule,
|
||||
HasRoleModule,
|
||||
MatChipsModule,
|
||||
MatButtonModule,
|
||||
MatCheckboxModule,
|
||||
MatIconModule,
|
||||
MatTableModule,
|
||||
MatPaginatorModule,
|
||||
MatFormFieldModule,
|
||||
MatSelectModule,
|
||||
MatSortModule,
|
||||
MatTooltipModule,
|
||||
ReactiveFormsModule,
|
||||
MatProgressSpinnerModule,
|
||||
FormsModule,
|
||||
TranslateModule,
|
||||
],
|
||||
})
|
||||
export class ProjectGrantDetailModule { }
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
<h1 mat-dialog-title>
|
||||
<span class="title">{{'MEMBER.ADD' | translate}}</span>
|
||||
</h1>
|
||||
<p class="desc"> {{'ORG_DETAIL.MEMBER.ADDDESCRIPTION' | translate}}</p>
|
||||
<div mat-dialog-content>
|
||||
<app-search-user-autocomplete (selectionChanged)="selectUsers($event)">
|
||||
</app-search-user-autocomplete>
|
||||
|
||||
<mat-form-field class="full-width" appearance="outline">
|
||||
<mat-label>{{ 'PROJECT.MEMBER.ROLES' | translate }}</mat-label>
|
||||
<mat-select [(ngModel)]="roleKeyList" multiple>
|
||||
<mat-option *ngFor="let key of data.roleKeysList" [value]="key">
|
||||
{{ key }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div mat-dialog-actions class="action">
|
||||
<button mat-button (click)="closeDialog()">
|
||||
cancel
|
||||
</button>
|
||||
|
||||
<button [disabled]="userIds.length == 0 || roleKeyList.length == 0" color="primary" mat-raised-button
|
||||
class="ok-button" (click)="closeDialogWithSuccess()">
|
||||
Add
|
||||
</button>
|
||||
</div>
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
.full-width {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.action {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
||||
.ok-button {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ProjectGrantMembersCreateDialogComponent } from './project-grant-members-create-dialog.component';
|
||||
|
||||
describe('ProjectGrantMembersCreateDialogComponent', () => {
|
||||
let component: ProjectGrantMembersCreateDialogComponent;
|
||||
let fixture: ComponentFixture<ProjectGrantMembersCreateDialogComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ProjectGrantMembersCreateDialogComponent],
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ProjectGrantMembersCreateDialogComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
import { Component, Inject } from '@angular/core';
|
||||
import { FormGroup } from '@angular/forms';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import { User } from 'src/app/proto/generated/management_pb';
|
||||
|
||||
export interface ProjectGrantMembersCreateDialogExportType {
|
||||
userIds: string[];
|
||||
rolesKeyList: string[];
|
||||
}
|
||||
@Component({
|
||||
selector: 'app-project-grant-members-create-dialog',
|
||||
templateUrl: './project-grant-members-create-dialog.component.html',
|
||||
styleUrls: ['./project-grant-members-create-dialog.component.scss'],
|
||||
})
|
||||
export class ProjectGrantMembersCreateDialogComponent {
|
||||
public form!: FormGroup;
|
||||
public userIds: string[] = [];
|
||||
public roleKeyList: string[] = [];
|
||||
|
||||
constructor(
|
||||
public dialogRef: MatDialogRef<ProjectGrantMembersCreateDialogComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: any,
|
||||
) { }
|
||||
|
||||
public selectUsers(users: User.AsObject[]): void {
|
||||
this.userIds = users.map(user => user.id);
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
this.dialogRef.close(false);
|
||||
}
|
||||
|
||||
public closeDialogWithSuccess(): void {
|
||||
const exportData: ProjectGrantMembersCreateDialogExportType = {
|
||||
userIds: this.userIds,
|
||||
rolesKeyList: this.roleKeyList,
|
||||
};
|
||||
this.dialogRef.close(exportData);
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatDialogModule } from '@angular/material/dialog';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { SearchUserAutocompleteModule } from 'src/app/modules/search-user-autocomplete/search-user-autocomplete.module';
|
||||
|
||||
import { ProjectGrantMembersCreateDialogComponent } from './project-grant-members-create-dialog.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [ProjectGrantMembersCreateDialogComponent],
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
MatDialogModule,
|
||||
MatButtonModule,
|
||||
TranslateModule,
|
||||
MatSelectModule,
|
||||
MatFormFieldModule,
|
||||
SearchUserAutocompleteModule,
|
||||
],
|
||||
})
|
||||
export class ProjectGrantMembersCreateDialogModule { }
|
||||
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
import { DataSource } from '@angular/cdk/collections';
|
||||
import { BehaviorSubject, from, Observable, of } from 'rxjs';
|
||||
import { catchError, finalize, map } from 'rxjs/operators';
|
||||
import { ProjectMember } from 'src/app/proto/generated/management_pb';
|
||||
import { ProjectService } from 'src/app/services/project.service';
|
||||
|
||||
/**
|
||||
* Data source for the ProjectMembers view. This class should
|
||||
* encapsulate all logic for fetching and manipulating the displayed data
|
||||
* (including sorting, pagination, and filtering).
|
||||
*/
|
||||
export class ProjectGrantMembersDataSource extends DataSource<ProjectMember.AsObject> {
|
||||
public totalResult: number = 0;
|
||||
public membersSubject: BehaviorSubject<ProjectMember.AsObject[]> = new BehaviorSubject<ProjectMember.AsObject[]>([]);
|
||||
private loadingSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
|
||||
public loading$: Observable<boolean> = this.loadingSubject.asObservable();
|
||||
|
||||
constructor(private projectService: ProjectService) {
|
||||
super();
|
||||
}
|
||||
|
||||
public loadGrantMembers(projectId: string, grantId: string, pageIndex: number,
|
||||
pageSize: number): void {
|
||||
const offset = pageIndex * pageSize;
|
||||
|
||||
this.loadingSubject.next(true);
|
||||
|
||||
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 => {
|
||||
this.membersSubject.next(members);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect this data source to the table. The table will only update when
|
||||
* the returned stream emits new items.
|
||||
* @returns A stream of the items to be rendered.
|
||||
*/
|
||||
public connect(): Observable<ProjectMember.AsObject[]> {
|
||||
return this.membersSubject.asObservable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the table is being destroyed. Use this function, to clean up
|
||||
* any open connections or free any held resources that were set up during connect.
|
||||
*/
|
||||
public disconnect(): void {
|
||||
this.membersSubject.complete();
|
||||
this.loadingSubject.complete();
|
||||
}
|
||||
}
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
<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?.membersSubject.value.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>
|
||||
<button (click)="removeProjectMemberSelection()" matTooltip="{{'ORG_DETAIL.TABLE.DELETE' | translate}}"
|
||||
class="icon-button" color="warn" mat-icon-button *ngIf="selection.hasValue()">
|
||||
<i class="las la-trash"></i>
|
||||
</button>
|
||||
<a color="primary" [disabled]="disabled" class="add-button" (click)="openAddMember()" 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="dataSource?.loading$ | async">
|
||||
<mat-spinner diameter="50"></mat-spinner>
|
||||
</div>
|
||||
<table mat-table class="full-width-table" aria-label="Elements"
|
||||
[ngClass]="{'background-style': type == ProjectType.PROJECTTYPE_OWNED}" [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="firstname">
|
||||
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.MEMBER.FIRSTNAME' | translate }} </th>
|
||||
<td class="pointer" [routerLink]="['/user', member.userId]" mat-cell *matCellDef="let member">
|
||||
{{member.firstName}} </td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="lastname">
|
||||
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.MEMBER.LASTNAME' | translate }} </th>
|
||||
<td class="pointer" [routerLink]="['/user', member.userId]" mat-cell *matCellDef="let member">
|
||||
{{member.lastName}} </td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="username">
|
||||
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.MEMBER.USERNAME' | translate }} </th>
|
||||
<td class="pointer" [routerLink]="['/user', member.userId]" mat-cell *matCellDef="let member">
|
||||
{{member.userName}} </td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="email">
|
||||
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.MEMBER.EMAIL' | translate }} </th>
|
||||
<td class="pointer" [routerLink]="['/user', member.userId]" mat-cell *matCellDef="let member">
|
||||
{{member.email}}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="roles">
|
||||
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.MEMBER.ROLES' | translate }} </th>
|
||||
<td class="pointer" mat-cell *matCellDef="let member">
|
||||
<mat-form-field class="form-field" appearance="outline" *ngIf="projectId">
|
||||
<mat-label>{{ 'PROJECT.MEMBER.ROLES' | translate }}</mat-label>
|
||||
<mat-select [(ngModel)]="member.rolesList" multiple [disabled]="disabled"
|
||||
(selectionChange)="updateRoles(member, $event)">
|
||||
<mat-option *ngFor="let role of memberRoleOptions" [value]="role">
|
||||
{{ 'ROLES.'+role | translate }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr class="data-row" mat-row *matRowDef="let row; columns: displayedColumns;">
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<mat-paginator [ngClass]="{'background-style': type == ProjectType.PROJECTTYPE_OWNED}" #paginator [pageSize]="50"
|
||||
[pageSizeOptions]="[25, 50, 100, 250]">
|
||||
</mat-paginator>
|
||||
</div>
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.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 {
|
||||
td, th {
|
||||
padding: .5rem;
|
||||
&:first-child {
|
||||
padding-left: 0;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.action {
|
||||
width: 40px;
|
||||
}
|
||||
|
||||
.data-row {
|
||||
&:hover {
|
||||
background-color: #ffffff05;
|
||||
}
|
||||
}
|
||||
|
||||
.selection {
|
||||
width: 50px;
|
||||
max-width: 50px;
|
||||
}
|
||||
|
||||
.role {
|
||||
display: inline-block;
|
||||
margin: .25rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pointer {
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { MatPaginatorModule } from '@angular/material/paginator';
|
||||
import { MatSortModule } from '@angular/material/sort';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
|
||||
import { ProjectGrantMembersComponent } from './project-grant-members.component';
|
||||
|
||||
describe('ProjectMembersComponent', () => {
|
||||
let component: ProjectGrantMembersComponent;
|
||||
let fixture: ComponentFixture<ProjectGrantMembersComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ProjectGrantMembersComponent],
|
||||
imports: [
|
||||
NoopAnimationsModule,
|
||||
MatPaginatorModule,
|
||||
MatSortModule,
|
||||
MatTableModule,
|
||||
],
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ProjectGrantMembersComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should compile', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
+154
@@ -0,0 +1,154 @@
|
||||
import { SelectionModel } from '@angular/cdk/collections';
|
||||
import { AfterViewInit, Component, Input, OnInit, ViewChild } from '@angular/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { MatPaginator } from '@angular/material/paginator';
|
||||
import { MatSelectChange } from '@angular/material/select';
|
||||
import { MatTable } from '@angular/material/table';
|
||||
import { tap } from 'rxjs/operators';
|
||||
import { ProjectMember, ProjectType } from 'src/app/proto/generated/management_pb';
|
||||
import { ProjectService } from 'src/app/services/project.service';
|
||||
import { ToastService } from 'src/app/services/toast.service';
|
||||
|
||||
import {
|
||||
ProjectGrantMembersCreateDialogComponent,
|
||||
ProjectGrantMembersCreateDialogExportType,
|
||||
} from './project-grant-members-create-dialog/project-grant-members-create-dialog.component';
|
||||
import { ProjectGrantMembersDataSource } from './project-grant-members-datasource';
|
||||
|
||||
@Component({
|
||||
selector: 'app-project-grant-members',
|
||||
templateUrl: './project-grant-members.component.html',
|
||||
styleUrls: ['./project-grant-members.component.scss'],
|
||||
})
|
||||
export class ProjectGrantMembersComponent implements AfterViewInit, OnInit {
|
||||
@Input() public projectId!: string;
|
||||
@Input() public grantId!: string;
|
||||
|
||||
@Input() public type: ProjectType = ProjectType.PROJECTTYPE_GRANTED;
|
||||
|
||||
@Input() public disabled: boolean = false;
|
||||
@ViewChild(MatPaginator) public paginator!: MatPaginator;
|
||||
@ViewChild(MatTable) public table!: MatTable<ProjectMember.AsObject>;
|
||||
public dataSource!: ProjectGrantMembersDataSource;
|
||||
public selection: SelectionModel<ProjectMember.AsObject> = new SelectionModel<ProjectMember.AsObject>(true, []);
|
||||
|
||||
/** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */
|
||||
public displayedColumns: string[] = ['select', 'firstname', 'lastname', 'username', 'email', 'roles'];
|
||||
|
||||
public ProjectType: any = ProjectType;
|
||||
public memberRoleOptions: string[] = [];
|
||||
|
||||
constructor(
|
||||
private projectService: ProjectService,
|
||||
private dialog: MatDialog,
|
||||
private toast: ToastService,
|
||||
) {
|
||||
this.dataSource = new ProjectGrantMembersDataSource(this.projectService);
|
||||
this.getRoleOptions();
|
||||
}
|
||||
|
||||
public ngOnInit(): void {
|
||||
this.dataSource.loadGrantMembers(this.projectId, this.grantId, 0, 25);
|
||||
}
|
||||
|
||||
public ngAfterViewInit(): void {
|
||||
this.paginator.page
|
||||
.pipe(
|
||||
tap(() => this.loadMembersPage()),
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
public getRoleOptions(): void {
|
||||
if (this.type === ProjectType.PROJECTTYPE_GRANTED) {
|
||||
this.projectService.GetProjectGrantMemberRoles().then(resp => {
|
||||
this.memberRoleOptions = resp.toObject().rolesList;
|
||||
}).catch(error => {
|
||||
this.toast.showError(error);
|
||||
});
|
||||
} else if (this.type === ProjectType.PROJECTTYPE_OWNED) {
|
||||
this.projectService.GetProjectMemberRoles().then(resp => {
|
||||
this.memberRoleOptions = resp.toObject().rolesList;
|
||||
}).catch(error => {
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private loadMembersPage(): void {
|
||||
this.dataSource.loadGrantMembers(
|
||||
this.projectId,
|
||||
this.grantId,
|
||||
this.paginator.pageIndex,
|
||||
this.paginator.pageSize,
|
||||
);
|
||||
}
|
||||
|
||||
public removeProjectMemberSelection(): void {
|
||||
Promise.all(this.selection.selected.map(member => {
|
||||
return this.projectService.RemoveProjectGrantMember(this.projectId, this.grantId, member.userId).then(() => {
|
||||
this.toast.showInfo('Removed successfully');
|
||||
}).catch(error => {
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
public removeMember(member: ProjectMember.AsObject): void {
|
||||
this.projectService.RemoveProjectGrantMember(this.projectId, this.grantId, member.userId).then(() => {
|
||||
this.toast.showInfo('Member removed successfully');
|
||||
}).catch(error => {
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}
|
||||
|
||||
public isAllSelected(): boolean {
|
||||
const numSelected = this.selection.selected.length;
|
||||
const numRows = this.dataSource.membersSubject.value.length;
|
||||
return numSelected === numRows;
|
||||
}
|
||||
|
||||
public masterToggle(): void {
|
||||
this.isAllSelected() ?
|
||||
this.selection.clear() :
|
||||
this.dataSource.membersSubject.value.forEach(row => this.selection.select(row));
|
||||
}
|
||||
|
||||
public async openAddMember(): Promise<any> {
|
||||
const keysList = (await this.projectService.GetProjectGrantMemberRoles()).toObject();
|
||||
|
||||
const dialogRef = this.dialog.open(ProjectGrantMembersCreateDialogComponent, {
|
||||
data: {
|
||||
roleKeysList: keysList.rolesList,
|
||||
},
|
||||
width: '400px',
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe((dataToAdd: ProjectGrantMembersCreateDialogExportType) => {
|
||||
if (dataToAdd) {
|
||||
Promise.all(dataToAdd.userIds.map((userid: string) => {
|
||||
return this.projectService.AddProjectGrantMember(
|
||||
this.projectId,
|
||||
this.grantId,
|
||||
userid,
|
||||
dataToAdd.rolesKeyList,
|
||||
);
|
||||
})).then(() => {
|
||||
console.log('this');
|
||||
this.toast.showInfo('Project Grant Member successfully added!');
|
||||
}).catch(error => {
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
updateRoles(member: ProjectMember.AsObject, selectionChange: MatSelectChange): void {
|
||||
this.projectService.ChangeProjectGrantMember(this.projectId, this.grantId, member.userId, selectionChange.value)
|
||||
.then((newmember: ProjectMember) => {
|
||||
this.toast.showInfo('Member updated!');
|
||||
}).catch(error => {
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
import { MatDialogModule } from '@angular/material/dialog';
|
||||
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 { 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 { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { HasRoleModule } from 'src/app/directives/has-role/has-role.module';
|
||||
import { SearchUserAutocompleteModule } from 'src/app/modules/search-user-autocomplete/search-user-autocomplete.module';
|
||||
|
||||
import {
|
||||
ProjectGrantMembersCreateDialogModule,
|
||||
} from './project-grant-members-create-dialog/project-grant-members-create-dialog.module';
|
||||
import { ProjectGrantMembersComponent } from './project-grant-members.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [ProjectGrantMembersComponent],
|
||||
imports: [
|
||||
CommonModule,
|
||||
HasRoleModule,
|
||||
RouterModule,
|
||||
MatButtonModule,
|
||||
MatCheckboxModule,
|
||||
MatIconModule,
|
||||
MatInputModule,
|
||||
MatFormFieldModule,
|
||||
MatSelectModule,
|
||||
MatTableModule,
|
||||
SearchUserAutocompleteModule,
|
||||
ProjectGrantMembersCreateDialogModule,
|
||||
MatPaginatorModule,
|
||||
MatSortModule,
|
||||
MatTooltipModule,
|
||||
MatDialogModule,
|
||||
ReactiveFormsModule,
|
||||
MatProgressSpinnerModule,
|
||||
FormsModule,
|
||||
TranslateModule,
|
||||
],
|
||||
exports: [
|
||||
ProjectGrantMembersComponent,
|
||||
],
|
||||
})
|
||||
export class ProjectGrantMembersModule { }
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
import { DataSource } from '@angular/cdk/collections';
|
||||
import { BehaviorSubject, from, Observable, of } from 'rxjs';
|
||||
import { catchError, finalize, map } from 'rxjs/operators';
|
||||
import { ProjectGrant } from 'src/app/proto/generated/management_pb';
|
||||
import { ProjectService } from 'src/app/services/project.service';
|
||||
|
||||
/**
|
||||
* Data source for the ProjectMembers view. This class should
|
||||
* encapsulate all logic for fetching and manipulating the displayed data
|
||||
* (including sorting, pagination, and filtering).
|
||||
*/
|
||||
export class ProjectGrantsDataSource extends DataSource<ProjectGrant.AsObject> {
|
||||
public grantsSubject: BehaviorSubject<ProjectGrant.AsObject[]> = new BehaviorSubject<ProjectGrant.AsObject[]>([]);
|
||||
private loadingSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
|
||||
public loading$: Observable<boolean> = this.loadingSubject.asObservable();
|
||||
|
||||
constructor(private projectService: ProjectService) {
|
||||
super();
|
||||
}
|
||||
|
||||
public loadGrants(projectId: string, pageIndex: number, pageSize: number, sortDirection?: string): void {
|
||||
const offset = pageIndex * pageSize;
|
||||
|
||||
this.loadingSubject.next(true);
|
||||
from(this.projectService.SearchProjectGrants(projectId, pageSize, offset)).pipe(
|
||||
map(resp => {
|
||||
return resp.toObject().resultList;
|
||||
}),
|
||||
catchError(() => of([])),
|
||||
finalize(() => this.loadingSubject.next(false)),
|
||||
).subscribe(grants => {
|
||||
console.log(grants);
|
||||
this.grantsSubject.next(grants);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Connect this data source to the table. The table will only update when
|
||||
* the returned stream emits new items.
|
||||
* @returns A stream of the items to be rendered.
|
||||
*/
|
||||
public connect(): Observable<ProjectGrant.AsObject[]> {
|
||||
return this.grantsSubject.asObservable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the table is being destroyed. Use this function, to clean up
|
||||
* any open connections or free any held resources that were set up during connect.
|
||||
*/
|
||||
public disconnect(): void {
|
||||
this.grantsSubject.complete();
|
||||
this.loadingSubject.complete();
|
||||
}
|
||||
}
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
<div class="table-header-row" *ngIf="projectId">
|
||||
<div class="col">
|
||||
<ng-container *ngIf="!selection.hasValue()">
|
||||
<span class="desc">{{'ORG_DETAIL.TABLE.TOTAL' | translate}}</span>
|
||||
<span class="count">{{dataSource.grantsSubject.value.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>
|
||||
<ng-template appHasRole [appHasRole]="['project.grant.member.delete:'+projectId, 'project.grant.member.delete']">
|
||||
<button (click)="deleteSelectedGrants()" [disabled]="disabled" class="icon-button" mat-icon-button
|
||||
*ngIf="selection.hasValue()" color="warn">
|
||||
<i class="las la-trash"></i>
|
||||
</button>
|
||||
</ng-template>
|
||||
<ng-template appHasRole [appHasRole]="['project.grant.member.write:'+projectId,'project.grant.member.write']">
|
||||
<a [disabled]="disabled" color="primary" class="add-button" color="primary" mat-raised-button
|
||||
[routerLink]="[ '/projects', projectId, 'grants', 'create']">
|
||||
<mat-icon class="icon">add</mat-icon>{{ 'ACTIONS.NEW' | translate }}
|
||||
</a>
|
||||
</ng-template>
|
||||
</div>
|
||||
|
||||
<div class="table-wrapper">
|
||||
<div class="spinner-container" *ngIf="dataSource.loading$ | async">
|
||||
<mat-spinner diameter="50"></mat-spinner>
|
||||
</div>
|
||||
<table mat-table multiTemplateDataRows class="full-width-table" aria-label="Elements" [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="grantedOrgName">
|
||||
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.GRANT.GRANTEDORG' | translate }} </th>
|
||||
<td [routerLink]="['/projects',grant.projectId,'grant', grant.id]" class="pointer" mat-cell
|
||||
*matCellDef="let grant">
|
||||
{{grant.grantedOrgName}} </td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="creationDate">
|
||||
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.GRANT.CREATIONDATE' | translate }} </th>
|
||||
<td [routerLink]="['/projects',grant.projectId,'grant', grant.id]" class="pointer" mat-cell
|
||||
*matCellDef="let grant">
|
||||
{{grant.creationDate | timestampToDate | date: 'dd. MMM, HH:mm' }} </td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="changeDate">
|
||||
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.GRANT.CHANGEDATE' | translate }} </th>
|
||||
<td [routerLink]="['/projects',grant.projectId,'grant', grant.id]" class="pointer" mat-cell
|
||||
*matCellDef="let grant">
|
||||
{{grant.changeDate | timestampToDate | date: 'dd. MMM, HH:mm' }} </td>
|
||||
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="roleNamesList">
|
||||
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.GRANT.ROLENAMESLIST' | translate }} </th>
|
||||
<td class="pointer" mat-cell *matCellDef="let grant">
|
||||
<mat-form-field class="form-field" appearance="outline" *ngIf="grant.roleKeysList">
|
||||
<mat-label>{{ 'PROJECT.GRANT.ROLENAMESLIST' | translate }}</mat-label>
|
||||
<mat-select [(ngModel)]="grant.roleKeysList" multiple [disabled]="disabled"
|
||||
(selectionChange)="updateRoles(grant, $event)">
|
||||
<mat-option *ngFor="let role of memberRoleOptions" [value]="role.key">
|
||||
{{role.key}}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr class="data-row" mat-row *matRowDef="let grant; columns: displayedColumns;" class="element-row">
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<mat-paginator #paginator [pageSize]="50" [pageSizeOptions]="[25, 50, 100, 250]">
|
||||
</mat-paginator>
|
||||
</div>
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
.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;
|
||||
}
|
||||
|
||||
.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 {
|
||||
padding: 0 1rem;
|
||||
|
||||
&:first-child {
|
||||
padding-left: 0;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.role {
|
||||
display: inline-block;
|
||||
border-radius: .5rem;
|
||||
border: 1px solid #ffffff30;
|
||||
padding: .5rem 1rem;
|
||||
margin-top: .25rem;
|
||||
margin-bottom: .25rem;
|
||||
margin-left: 0;
|
||||
margin-right: .5rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
tr {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
tr.detail-row {
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.selection {
|
||||
width: 50px;
|
||||
max-width: 50px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pointer {
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ProjectGrantsComponent } from './project-grants.component';
|
||||
|
||||
describe('ProjectGrantsComponent', () => {
|
||||
let component: ProjectGrantsComponent;
|
||||
let fixture: ComponentFixture<ProjectGrantsComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ProjectGrantsComponent],
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ProjectGrantsComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
import { animate, state, style, transition, trigger } from '@angular/animations';
|
||||
import { SelectionModel } from '@angular/cdk/collections';
|
||||
import { AfterViewInit, Component, Input, OnInit, ViewChild } from '@angular/core';
|
||||
import { MatPaginator } from '@angular/material/paginator';
|
||||
import { MatSelectChange } from '@angular/material/select';
|
||||
import { MatTable } from '@angular/material/table';
|
||||
import { tap } from 'rxjs/operators';
|
||||
import { ProjectGrant, ProjectRoleView } from 'src/app/proto/generated/management_pb';
|
||||
import { ProjectService } from 'src/app/services/project.service';
|
||||
import { ToastService } from 'src/app/services/toast.service';
|
||||
|
||||
import { ProjectGrantsDataSource } from './project-grants-datasource';
|
||||
|
||||
@Component({
|
||||
selector: 'app-project-grants',
|
||||
templateUrl: './project-grants.component.html',
|
||||
styleUrls: ['./project-grants.component.scss'],
|
||||
animations: [
|
||||
trigger('detailExpand', [
|
||||
state('collapsed', style({ height: '0px', minHeight: '0' })),
|
||||
state('expanded', style({ height: '*' })),
|
||||
transition('expanded <=> collapsed', animate('225ms cubic-bezier(0.4, 0.0, 0.2, 1)')),
|
||||
]),
|
||||
],
|
||||
})
|
||||
export class ProjectGrantsComponent implements OnInit, AfterViewInit {
|
||||
@Input() public projectId: string = '';
|
||||
@Input() public disabled: boolean = false;
|
||||
@ViewChild(MatPaginator) public paginator!: MatPaginator;
|
||||
@ViewChild(MatTable) public table!: MatTable<ProjectGrant.AsObject>;
|
||||
public dataSource!: ProjectGrantsDataSource;
|
||||
public selection: SelectionModel<ProjectGrant.AsObject> = new SelectionModel<ProjectGrant.AsObject>(true, []);
|
||||
public memberRoleOptions: ProjectRoleView.AsObject[] = [];
|
||||
|
||||
/** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */
|
||||
public displayedColumns: string[] = ['select', 'grantedOrgName', 'creationDate', 'changeDate', 'roleNamesList'];
|
||||
|
||||
constructor(private projectService: ProjectService, private toast: ToastService) { }
|
||||
|
||||
public ngOnInit(): void {
|
||||
this.dataSource = new ProjectGrantsDataSource(this.projectService);
|
||||
this.dataSource.loadGrants(this.projectId, 0, 25, 'asc');
|
||||
this.getRoleOptions(this.projectId);
|
||||
}
|
||||
|
||||
public ngAfterViewInit(): void {
|
||||
this.paginator.page
|
||||
.pipe(
|
||||
tap(() => this.loadGrantsPage()),
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
}
|
||||
|
||||
private loadGrantsPage(): void {
|
||||
this.dataSource.loadGrants(
|
||||
this.projectId,
|
||||
this.paginator.pageIndex,
|
||||
this.paginator.pageSize,
|
||||
);
|
||||
}
|
||||
|
||||
public isAllSelected(): boolean {
|
||||
const numSelected = this.selection.selected.length;
|
||||
const numRows = this.dataSource.grantsSubject.value.length;
|
||||
return numSelected === numRows;
|
||||
}
|
||||
|
||||
public masterToggle(): void {
|
||||
this.isAllSelected() ?
|
||||
this.selection.clear() :
|
||||
this.dataSource.grantsSubject.value.forEach(row => this.selection.select(row));
|
||||
}
|
||||
|
||||
public getRoleOptions(projectId: string): void {
|
||||
this.projectService.SearchProjectRoles(projectId, 100, 0).then(resp => {
|
||||
this.memberRoleOptions = resp.toObject().resultList;
|
||||
console.log(resp.toObject());
|
||||
});
|
||||
}
|
||||
|
||||
updateRoles(grant: ProjectGrant.AsObject, selectionChange: MatSelectChange): void {
|
||||
this.projectService.UpdateProjectGrant(grant.id, grant.projectId, selectionChange.value)
|
||||
.then((newgrant: ProjectGrant) => {
|
||||
this.toast.showInfo('Grant updated!');
|
||||
}).catch(error => {
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}
|
||||
|
||||
deleteSelectedGrants(): void {
|
||||
const promises = this.selection.selected.map(grant => {
|
||||
return this.projectService.RemoveProjectGrant(grant.id, grant.projectId);
|
||||
});
|
||||
|
||||
Promise.all(promises).then(() => {
|
||||
this.toast.showInfo('GRANTS.TOAST.BULKREMOVED', true);
|
||||
const data = this.dataSource.grantsSubject.getValue();
|
||||
this.selection.selected.forEach((item) => {
|
||||
const index = data.findIndex(i => i.id === item.id);
|
||||
if (index > -1) {
|
||||
data.splice(index, 1);
|
||||
this.dataSource.grantsSubject.next(data);
|
||||
}
|
||||
});
|
||||
this.selection.clear();
|
||||
}).catch(error => {
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user