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

* pinned organisations

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

* rm entry components

* members nav, i18n, disable actions on non active

* add org domain dialog

* mv password policy rm to detail view

* prefix pinned orgs for userid, fix collapsed pad

* fix app back navigation

* rem pwd required validator

* fix org item overflow

* routing

* move users modules to users page

* reorganize projects

* remove child init of translate

* hide same preferred loginname
This commit is contained in:
Max Peintner
2020-07-13 13:38:49 +02:00
committed by GitHub
parent d8eef34a37
commit b628baeb1a
193 changed files with 967 additions and 500 deletions
@@ -0,0 +1,86 @@
<div class="container">
<div class="abort-container">
<button (click)="close()" mat-icon-button>
<mat-icon>close</mat-icon>
</button>
<span class="abort">{{ 'APP.PAGES.CREATE_OIDC' | translate }}</span><span class="abort-2">Step
{{ currentCreateStep }} of
{{ createSteps }}</span>
</div>
<h1>{{'APP.PAGES.CREATE_OIDC_DESc' | translate}}</h1>
<form [formGroup]="form" (ngSubmit)="saveOIDCApp()">
<div class="content">
<mat-form-field appearance="outline" class="formfield">
<mat-label>{{ 'APP.NAME' | translate }}</mat-label>
<input matInput formControlName="name" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>{{ 'APP.OIDC.RESPONSE' | translate }}</mat-label>
<mat-select formControlName="responseTypesList" multiple>
<mat-option *ngFor="let type of oidcResponseTypes" [value]="type">
{{ 'APP.OIDC.RESPONSE'+type | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>{{ 'APP.OIDC.GRANT' | translate }}</mat-label>
<mat-select formControlName="grantTypesList" multiple>
<mat-option *ngFor="let grant of oidcGrantTypes" [value]="grant">
{{ 'APP.OIDC.GRANT'+grant | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline" class="formfield">
<mat-label>{{ 'APP.OIDC.APPTYPE' | translate }}</mat-label>
<mat-select formControlName="applicationType">
<mat-option *ngFor="let type of oidcAppTypes" [value]="type">
{{ 'APP.OIDC.APPTYPE'+type | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline" class="formfield">
<mat-label>{{ 'APP.OIDC.AUTHMETHOD' | translate }}</mat-label>
<mat-select formControlName="authMethodType">
<mat-option *ngFor="let type of oidcAuthMethodType" [value]="type">
{{ 'APP.OIDC.AUTHMETHOD'+type | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline" class="full-width">
<mat-label>{{ 'APP.OIDC.REDIRECT' | translate }}</mat-label>
<mat-chip-list #chipRedirectList aria-label="uri selection">
<mat-chip *ngFor="let uri of oidcApp.redirectUrisList" removable
(removed)="removeUri(uri, 'REDIRECT')">
{{uri}} <mat-icon matChipRemove>cancel</mat-icon>
</mat-chip>
<input [matChipInputFor]="chipRedirectList" [matChipInputSeparatorKeyCodes]="separatorKeysCodes"
[matChipInputAddOnBlur]="addOnBlur" (matChipInputTokenEnd)="addUri($event, 'REDIRECT')">
</mat-chip-list>
</mat-form-field>
<mat-form-field appearance="outline" class="full-width">
<mat-label>{{ 'APP.OIDC.POSTLOGOUTREDIRECT' | translate }}</mat-label>
<mat-chip-list #chipPostRedirectList aria-label="uri selection">
<mat-chip *ngFor="let uri of oidcApp.postLogoutRedirectUrisList" removable
(removed)="removeUri(uri, 'POSTREDIRECT')">
{{uri}} <mat-icon matChipRemove>cancel</mat-icon>
</mat-chip>
<input [matChipInputFor]="chipPostRedirectList" [matChipInputSeparatorKeyCodes]="separatorKeysCodes"
[matChipInputAddOnBlur]="addOnBlur" (matChipInputTokenEnd)="addUri($event, 'POSTREDIRECT')">
</mat-chip-list>
</mat-form-field>
</div>
<button color="primary" mat-raised-button class="continue-button" [disabled]="form.invalid" cdkFocusInitial
type="submit">
{{ 'ACTIONS.SAVE' | translate }}
</button>
</form>
</div>
@@ -0,0 +1,75 @@
h1 {
font-weight: 500;
}
.container {
padding: 4rem 4rem 2rem 4rem;
.abort-container {
display: flex;
align-items: center;
margin-bottom: 2rem;
.abort {
font-size: 1.2rem;
margin-left: 2rem;
}
.abort-2 {
font-size: 1.2rem;
margin-left: 2rem;
}
}
}
.margin-right {
margin-right: 0.5rem;
}
.column {
display: flex;
flex-direction: column;
.formfield {
width: 400px;
input {
font-size: 1.5rem;
}
&.autocomplete {
margin-top: 1rem;
}
}
.slider {
margin-top: 2rem;
margin-bottom: 1rem;
font-size: 0.8rem;
}
.access-token {
width: 400px;
}
}
.content {
display: flex;
margin: 0 -.5rem;
flex-wrap: wrap;
mat-form-field {
flex: 1 0 40%;
margin: 0 .5rem;
}
.full-width {
flex-basis: 80%;
}
}
.continue-button {
margin-top: 3rem;
display: block;
padding: 0.5rem 4rem;
border-radius: 0.5rem;
}
@@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { AppCreateComponent } from './app-create.component';
describe('AppCreateComponent', () => {
let component: AppCreateComponent;
let fixture: ComponentFixture<AppCreateComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [AppCreateComponent],
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(AppCreateComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,184 @@
import { COMMA, ENTER, SPACE } from '@angular/cdk/keycodes';
import { Location } from '@angular/common';
import { Component, OnDestroy, OnInit } from '@angular/core';
import { AbstractControl, FormBuilder, FormGroup, Validators } from '@angular/forms';
import { MatChipInputEvent } from '@angular/material/chips';
import { MatDialog } from '@angular/material/dialog';
import { ActivatedRoute, Params, Router } from '@angular/router';
import { Subscription } from 'rxjs';
import {
Application,
OIDCApplicationCreate,
OIDCApplicationType,
OIDCAuthMethodType,
OIDCGrantType,
OIDCResponseType,
} from 'src/app/proto/generated/management_pb';
import { ProjectService } from 'src/app/services/project.service';
import { ToastService } from 'src/app/services/toast.service';
import { AppSecretDialogComponent } from '../app-secret-dialog/app-secret-dialog.component';
@Component({
selector: 'app-app-create',
templateUrl: './app-create.component.html',
styleUrls: ['./app-create.component.scss'],
})
export class AppCreateComponent implements OnInit, OnDestroy {
private subscription?: Subscription;
public projectId: string = '';
public oidcApp: OIDCApplicationCreate.AsObject = new OIDCApplicationCreate().toObject();
public oidcResponseTypes: OIDCResponseType[] = [
OIDCResponseType.OIDCRESPONSETYPE_CODE,
OIDCResponseType.OIDCRESPONSETYPE_ID_TOKEN,
OIDCResponseType.OIDCRESPONSETYPE_ID_TOKEN_TOKEN,
];
public oidcGrantTypes: OIDCGrantType[] = [
OIDCGrantType.OIDCGRANTTYPE_AUTHORIZATION_CODE,
OIDCGrantType.OIDCGRANTTYPE_IMPLICIT,
OIDCGrantType.OIDCGRANTTYPE_REFRESH_TOKEN,
];
public oidcAppTypes: OIDCApplicationType[] = [
OIDCApplicationType.OIDCAPPLICATIONTYPE_WEB,
OIDCApplicationType.OIDCAPPLICATIONTYPE_USER_AGENT,
OIDCApplicationType.OIDCAPPLICATIONTYPE_NATIVE,
];
public oidcAuthMethodType: OIDCAuthMethodType[] = [
OIDCAuthMethodType.OIDCAUTHMETHODTYPE_BASIC,
OIDCAuthMethodType.OIDCAUTHMETHODTYPE_NONE,
OIDCAuthMethodType.OIDCAUTHMETHODTYPE_POST,
];
public form!: FormGroup;
public createSteps: number = 1;
public currentCreateStep: number = 1;
public postLogoutRedirectUrisList: string[] = [];
public addOnBlur: boolean = true;
public readonly separatorKeysCodes: number[] = [ENTER, COMMA, SPACE];
constructor(
private router: Router,
private route: ActivatedRoute,
private toast: ToastService,
private dialog: MatDialog,
private projectService: ProjectService,
private fb: FormBuilder,
private _location: Location,
) {
this.form = this.fb.group({
name: ['', [Validators.required]],
responseTypesList: ['', []],
grantTypesList: ['', []],
applicationType: ['', []],
authMethodType: [],
});
}
public ngOnInit(): void {
this.subscription = this.route.params.subscribe(params => this.getData(params));
}
public ngOnDestroy(): void {
this.subscription?.unsubscribe();
}
private async getData({ projectid }: Params): Promise<void> {
this.projectId = projectid;
this.oidcApp.projectId = projectid;
}
public close(): void {
this._location.back();
}
public saveOIDCApp(): void {
this.oidcApp.name = this.name?.value;
this.oidcApp.applicationType = this.applicationType?.value;
this.oidcApp.grantTypesList = this.grantTypesList?.value;
this.oidcApp.responseTypesList = this.responseTypesList?.value;
this.oidcApp.authMethodType = this.authMethodType?.value;
this.projectService
.CreateOIDCApp(this.oidcApp)
.then((data: Application) => {
this.showSavedDialog(data.toObject());
})
.catch(data => {
console.error(data);
this.toast.showError(data.message);
});
}
public showSavedDialog(app: Application.AsObject): void {
if (app.oidcConfig !== undefined) {
const dialogRef = this.dialog.open(AppSecretDialogComponent, {
data: app.oidcConfig,
});
dialogRef.afterClosed().subscribe(result => {
this.router.navigate(['projects', this.projectId, 'apps', app.id]);
});
} else {
this.router.navigate(['projects', this.projectId, 'apps', app.id]);
}
}
public addUri(event: MatChipInputEvent, target: string): void {
const input = event.input;
const value = event.value.trim();
if (value !== '') {
if (target === 'REDIRECT') {
this.oidcApp.redirectUrisList.push(value);
} else if (target === 'POSTREDIRECT') {
this.oidcApp.postLogoutRedirectUrisList.push(value);
}
}
if (input) {
input.value = '';
}
}
public removeUri(uri: string, target: string): void {
if (target === 'REDIRECT') {
const index = this.oidcApp.redirectUrisList.indexOf(uri);
if (index !== undefined && index >= 0) {
this.oidcApp.redirectUrisList.splice(index, 1);
}
} else if (target === 'POSTREDIRECT') {
const index = this.oidcApp.postLogoutRedirectUrisList.indexOf(uri);
if (index !== undefined && index >= 0) {
this.oidcApp.postLogoutRedirectUrisList.splice(index, 1);
}
}
}
get name(): AbstractControl | null {
return this.form.get('name');
}
get responseTypesList(): AbstractControl | null {
return this.form.get('responseTypesList');
}
get grantTypesList(): AbstractControl | null {
return this.form.get('grantTypesList');
}
get applicationType(): AbstractControl | null {
return this.form.get('applicationType');
}
get authMethodType(): AbstractControl | null {
return this.form.get('authMethodType');
}
}
@@ -0,0 +1,122 @@
<div class="max-width-container">
<div class="head">
<a [routerLink]="['/projects', projectId]" mat-icon-button>
<mat-icon class="icon">arrow_back</mat-icon>
</a>
<h1>{{ 'APP.PAGES.TITLE' | translate }} {{app?.name}}</h1>
<p class="desc">{{ 'APP.PAGES.DESCRIPTION' | translate }}</p>
<p *ngIf="isZitadel" class="zitadel-warning">{{'PROJECT.PAGES.ZITADELPROJECT' | translate}}</p>
</div>
<span *ngIf="errorMessage" class="err-container">{{errorMessage}}</span>
<app-card title="{{ 'APP.PAGES.DETAIL.TITLE' | translate }}" *ngIf="app">
<form [formGroup]="appNameForm" (ngSubmit)="saveOIDCApp()">
<div class="content">
<mat-button-toggle-group formControlName="state" class="toggle" (change)="changeState($event)">
<mat-button-toggle [value]="AppState.APPSTATE_INACTIVE" matTooltip="Deactivate Org">
{{'APP.PAGES.DETAIL.STATE.'+AppState.APPSTATE_INACTIVE | translate}}
</mat-button-toggle>
<mat-button-toggle [value]="AppState.APPSTATE_ACTIVE" matTooltip="Activate Org">
{{'APP.PAGES.DETAIL.STATE.'+AppState.APPSTATE_ACTIVE | translate}}
</mat-button-toggle>
</mat-button-toggle-group>
<mat-form-field class="formfield">
<mat-label>{{ 'APP.NAME' | translate }}</mat-label>
<input matInput formControlName="name" />
</mat-form-field>
</div>
<div class="btn-container">
<button type="submit" color="primary" [disabled]="appNameForm.invalid || name?.disabled"
mat-raised-button>{{ 'ACTIONS.SAVE' | translate }}</button>
</div>
</form>
</app-card>
<app-card title="{{ 'APP.OIDC.TITLE' | translate }}" *ngIf="app && app.oidcConfig">
<card-actions class="card-actions">
<button mat-stroked-button
(click)="regenerateOIDCClientSecret()">{{'APP.OIDC.REGENERATESECRET' | translate}}</button>
</card-actions>
<form *ngIf="appForm" [formGroup]="appForm" (ngSubmit)="saveOIDCApp()">
<div class="content">
<mat-form-field appearance="outline">
<mat-label>{{ 'APP.OIDC.CLIENTID' | translate }}</mat-label>
<input matInput formControlName="clientId" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>{{ 'APP.OIDC.RESPONSE' | translate }}</mat-label>
<mat-select formControlName="responseTypesList" multiple>
<mat-option *ngFor="let type of oidcResponseTypes" [value]="type">
{{ 'APP.OIDC.RESPONSE'+type | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>{{ 'APP.OIDC.GRANT' | translate }}</mat-label>
<mat-select formControlName="grantTypesList" multiple>
<mat-option *ngFor="let grant of oidcGrantTypes" [value]="grant">
{{ 'APP.OIDC.GRANT'+grant | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline" class="formfield">
<mat-label>{{ 'APP.OIDC.APPTYPE' | translate }}</mat-label>
<mat-select formControlName="applicationType">
<mat-option *ngFor="let type of oidcAppTypes" [value]="type">
{{ 'APP.OIDC.APPTYPE'+type | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline" class="formfield">
<mat-label>{{ 'APP.OIDC.AUTHMETHOD' | translate }}</mat-label>
<mat-select formControlName="authMethodType">
<mat-option *ngFor="let type of oidcAuthMethodType" [value]="type">
{{ 'APP.OIDC.AUTHMETHOD'+type | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="full-width" appearance="outline">
<mat-label>{{ 'APP.OIDC.REDIRECT' | translate }}</mat-label>
<mat-chip-list #chipRedirectList>
<mat-chip *ngFor="let redirect of redirectUrisList" [selectable]="selectable"
(removed)="remove(redirect, RedirectType.REDIRECT)">
{{redirect}}
<mat-icon matChipRemove *ngIf="removable">cancel</mat-icon>
</mat-chip>
<input [matChipInputFor]="chipRedirectList" [matChipInputSeparatorKeyCodes]="separatorKeysCodes"
[matChipInputAddOnBlur]="addOnBlur"
(matChipInputTokenEnd)="add($event, RedirectType.REDIRECT)">
</mat-chip-list>
</mat-form-field>
<mat-form-field class="full-width" appearance="outline">
<mat-label>{{ 'APP.OIDC.POSTLOGOUTREDIRECT' | translate }}</mat-label>
<mat-chip-list #chipPostRedirectList>
<mat-chip *ngFor="let redirect of postLogoutRedirectUrisList" [selectable]="selectable"
(removed)="remove(redirect, RedirectType.POSTREDIRECT)">
{{redirect}}
<mat-icon matChipRemove *ngIf="removable">cancel</mat-icon>
</mat-chip>
<input [matChipInputFor]="chipPostRedirectList"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes" [matChipInputAddOnBlur]="addOnBlur"
(matChipInputTokenEnd)="add($event, RedirectType.POSTREDIRECT)">
</mat-chip-list>
</mat-form-field>
</div>
<div class="btn-container">
<button type="submit" color="primary" [disabled]="appForm.invalid"
mat-raised-button>{{ 'ACTIONS.SAVE' | translate }}</button>
</div>
</form>
</app-card>
</div>
@@ -0,0 +1,87 @@
.head {
display: flex;
align-items: center;
border-bottom: 1px solid #ffffff20;
flex-wrap: wrap;
a {
display: block;
}
h1 {
font-size: 1.2rem;
margin: 0 1rem;
margin-left: 2rem;
font-weight: normal;
}
.desc {
width: 100%;
display: block;
font-size: .9rem;
color: #8795a1;
}
.zitadel-warning {
font-size: 14px;
color: rgb(201,51,71);
}
}
.err-container {
color: rgb(201,51,71);
font-size: 14px;
}
.card-actions {
button {
border-radius: .5rem;
}
}
.content {
display: flex;
flex-direction: row;
flex-wrap: wrap;
margin: 0 -.5rem;
&.nowrap{
flex-wrap: nowrap;
}
&.center {
align-items: center;
}
mat-form-field {
flex: 1 1 30%;
margin: 0 .5rem;
}
.full-width {
flex-basis: 100%;
}
.toggle {
align-self: flex-start;
margin-bottom: 1rem;
margin-right: 1rem;
border-radius: .5rem;
i {
margin-right: 1rem;
}
}
}
.btn-container {
display: flex;
justify-content: flex-end;
margin: 0 -.5rem;
button {
border-radius: .5rem;
margin: 0 .5rem;
}
}
@@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { AppDetailComponent } from './app-detail.component';
describe('AppDetailComponent', () => {
let component: AppDetailComponent;
let fixture: ComponentFixture<AppDetailComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [AppDetailComponent],
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(AppDetailComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,277 @@
import { COMMA, ENTER, SPACE } from '@angular/cdk/keycodes';
import { Location } from '@angular/common';
import { Component, OnDestroy, OnInit } from '@angular/core';
import { AbstractControl, FormBuilder, FormGroup, Validators } from '@angular/forms';
import { MatButtonToggleChange } from '@angular/material/button-toggle';
import { MatChipInputEvent } from '@angular/material/chips';
import { MatDialog } from '@angular/material/dialog';
import { ActivatedRoute, Params } from '@angular/router';
import { TranslateService } from '@ngx-translate/core';
import { Subscription } from 'rxjs';
import {
Application,
AppState,
OIDCApplicationType,
OIDCAuthMethodType,
OIDCConfig,
OIDCGrantType,
OIDCResponseType,
} from 'src/app/proto/generated/management_pb';
import { GrpcService } from 'src/app/services/grpc.service';
import { OrgService } from 'src/app/services/org.service';
import { ProjectService } from 'src/app/services/project.service';
import { ToastService } from 'src/app/services/toast.service';
import { AppSecretDialogComponent } from '../app-secret-dialog/app-secret-dialog.component';
enum RedirectType {
REDIRECT = 'redirect',
POSTREDIRECT = 'postredirect',
}
@Component({
selector: 'app-app-detail',
templateUrl: './app-detail.component.html',
styleUrls: ['./app-detail.component.scss'],
})
export class AppDetailComponent implements OnInit, OnDestroy {
public errorMessage: string = '';
public selectable: boolean = false;
public removable: boolean = true;
public addOnBlur: boolean = true;
public readonly separatorKeysCodes: number[] = [ENTER, COMMA, SPACE];
private subscription?: Subscription;
public projectId: string = '';
public app!: Application.AsObject;
public oidcResponseTypes: OIDCResponseType[] = [
OIDCResponseType.OIDCRESPONSETYPE_CODE,
OIDCResponseType.OIDCRESPONSETYPE_ID_TOKEN,
OIDCResponseType.OIDCRESPONSETYPE_ID_TOKEN_TOKEN,
];
public oidcGrantTypes: OIDCGrantType[] = [
OIDCGrantType.OIDCGRANTTYPE_AUTHORIZATION_CODE,
OIDCGrantType.OIDCGRANTTYPE_IMPLICIT,
OIDCGrantType.OIDCGRANTTYPE_REFRESH_TOKEN,
];
public oidcAppTypes: OIDCApplicationType[] = [
OIDCApplicationType.OIDCAPPLICATIONTYPE_WEB,
OIDCApplicationType.OIDCAPPLICATIONTYPE_USER_AGENT,
OIDCApplicationType.OIDCAPPLICATIONTYPE_NATIVE,
];
public oidcAuthMethodType: OIDCAuthMethodType[] = [
OIDCAuthMethodType.OIDCAUTHMETHODTYPE_BASIC,
OIDCAuthMethodType.OIDCAUTHMETHODTYPE_POST,
OIDCAuthMethodType.OIDCAUTHMETHODTYPE_NONE,
];
public AppState: any = AppState;
public appNameForm!: FormGroup;
public appForm!: FormGroup;
public redirectUrisList: string[] = [];
public postLogoutRedirectUrisList: string[] = [];
public RedirectType: any = RedirectType;
public isZitadel: boolean = false;
constructor(
public translate: TranslateService,
private route: ActivatedRoute,
private toast: ToastService,
private projectService: ProjectService,
private fb: FormBuilder,
private _location: Location,
private dialog: MatDialog,
private grpcService: GrpcService,
private orgService: OrgService,
) {
this.appNameForm = this.fb.group({
state: ['', []],
name: ['', [Validators.required]],
});
this.appForm = this.fb.group({
clientId: [{ value: '', disabled: true }],
responseTypesList: [],
grantTypesList: [],
applicationType: [],
authMethodType: [],
});
}
public ngOnInit(): void {
this.subscription = this.route.params.subscribe(params => this.getData(params));
}
public ngOnDestroy(): void {
this.subscription?.unsubscribe();
}
private async getData({ projectid, id }: Params): Promise<void> {
this.projectId = projectid;
this.orgService.GetIam().then(iam => {
this.isZitadel = iam.toObject().iamProjectId === this.projectId;
});
this.projectService.GetApplicationById(projectid, id).then(app => {
this.app = app.toObject();
this.appNameForm.patchValue(this.app);
if (this.app.state !== AppState.APPSTATE_ACTIVE) {
this.appNameForm.controls['name'].disable();
this.appForm.disable();
} else {
this.appNameForm.controls['name'].enable();
this.appForm.enable();
}
if (this.app.oidcConfig?.redirectUrisList) {
this.redirectUrisList = this.app.oidcConfig.redirectUrisList;
}
if (this.app.oidcConfig?.postLogoutRedirectUrisList) {
this.postLogoutRedirectUrisList = this.app.oidcConfig.postLogoutRedirectUrisList;
}
if (this.app.oidcConfig) {
this.appForm.patchValue(this.app.oidcConfig);
}
}).catch(error => {
console.error(error);
this.toast.showError(error);
this.errorMessage = error.message;
});
}
public changeState(event: MatButtonToggleChange): void {
if (event.value === AppState.APPSTATE_ACTIVE) {
this.projectService.ReactivateApplication(this.projectId, this.app.id).then(() => {
this.toast.showInfo('APP.TOAST.REACTIVATED', true);
}).catch((error: any) => {
this.toast.showError(error);
});
} else if (event.value === AppState.APPSTATE_INACTIVE) {
this.projectService.DeactivateApplication(this.projectId, this.app.id).then(() => {
this.toast.showInfo('APP.TOAST.REACTIVATED', true);
}).catch((error: any) => {
this.toast.showError(error);
});
}
if (event.value !== AppState.APPSTATE_ACTIVE) {
this.appNameForm.controls['name'].disable();
this.appForm.disable();
} else {
this.appNameForm.controls['name'].enable();
this.appForm.enable();
this.clientId?.disable();
}
}
public add(event: MatChipInputEvent, target: RedirectType): void {
if (target === RedirectType.POSTREDIRECT) {
const input = event.input;
if (event.value !== '' && event.value !== ' ' && event.value !== '/') {
this.postLogoutRedirectUrisList.push(event.value);
}
if (input) {
input.value = '';
}
} else if (target === RedirectType.REDIRECT) {
const input = event.input;
if (event.value !== '' && event.value !== ' ' && event.value !== '/') {
this.redirectUrisList.push(event.value);
}
if (input) {
input.value = '';
}
}
}
public remove(redirect: any, target: RedirectType): void {
if (target === RedirectType.POSTREDIRECT) {
const index = this.postLogoutRedirectUrisList.indexOf(redirect);
if (index >= 0) {
this.postLogoutRedirectUrisList.splice(index, 1);
}
} else if (target === RedirectType.REDIRECT) {
const index = this.redirectUrisList.indexOf(redirect);
if (index >= 0) {
this.redirectUrisList.splice(index, 1);
}
}
}
public saveOIDCApp(): void {
if (this.appNameForm.valid) {
this.app.name = this.name?.value;
}
if (this.appForm.valid) {
if (this.app.oidcConfig) {
this.app.oidcConfig.responseTypesList = this.responseTypesList?.value;
this.app.oidcConfig.grantTypesList = this.grantTypesList?.value;
this.app.oidcConfig.applicationType = this.applicationType?.value;
this.app.oidcConfig.authMethodType = this.authMethodType?.value;
this.app.oidcConfig.redirectUrisList = this.redirectUrisList;
this.app.oidcConfig.postLogoutRedirectUrisList = this.postLogoutRedirectUrisList;
this.projectService
.UpdateOIDCAppConfig(this.projectId, this.app.id, this.app.oidcConfig)
.then((data: OIDCConfig) => {
this.toast.showInfo('APP.TOAST.OIDCUPDATED', true);
})
.catch(data => {
this.toast.showError(data.message);
});
}
}
}
public regenerateOIDCClientSecret(): void {
this.projectService.RegenerateOIDCClientSecret(this.app.id, this.projectId).then((data: OIDCConfig) => {
this.toast.showInfo('APP.TOAST.OIDCCLIENTSECRETREGENERATED', true);
this.dialog.open(AppSecretDialogComponent, {
data: {
clientId: data.toObject().clientId,
clientSecret: data.toObject().clientSecret,
},
width: '400px',
});
}).catch(error => {
this.toast.showError(error);
});
}
public navigateBack(): void {
this._location.back();
}
public get name(): AbstractControl | null {
return this.appNameForm.get('name');
}
public get clientId(): AbstractControl | null {
return this.appForm.get('clientId');
}
public get responseTypesList(): AbstractControl | null {
return this.appForm.get('responseTypesList');
}
public get grantTypesList(): AbstractControl | null {
return this.appForm.get('grantTypesList');
}
public get applicationType(): AbstractControl | null {
return this.appForm.get('applicationType');
}
public get authMethodType(): AbstractControl | null {
return this.appForm.get('authMethodType');
}
}
@@ -0,0 +1,22 @@
<h1 mat-dialog-title>
<span class="title">{{'APP.OIDC.CLIENTSECRET' | translate}}</span>
</h1>
<p class="desc">{{'APP.OIDC.CLIENTSECRET_DESCRIPTION' | translate}}</p>
<div mat-dialog-content>
<p *ngIf="data.clientId">ClientId: {{data.clientId}}</p>
<div *ngIf="data.clientSecret" class="flex">
<button color="primary" [disabled]="copied == data.clientSecret" matTooltip="copy to clipboard"
(click)="copytoclipboard(data.clientSecret)" mat-icon-button>
<i *ngIf="copied != data.clientSecret" class="las la-clipboard"></i>
<i *ngIf="copied == data.clientSecret" class="las la-clipboard-check"></i>
</button>
<span class="secret">{{data.clientSecret}}</span>
</div>
</div>
<div mat-dialog-actions class="action">
<button cdkFocusInitial color="primary" mat-raised-button class="ok-button" (click)="closeDialog()">
{{'ACTIONS.CLOSE' | translate}}
</button>
</div>
@@ -0,0 +1,37 @@
.title {
font-size: 1.2rem;
}
.desc {
color: #8795a1;
font-size: .9rem;
}
.full-width {
width: 100%;
}
.action {
display: flex;
justify-content: flex-end;
.ok-button {
margin-left: 0.5rem;
}
button {
border-radius: 0.5rem;
}
}
.flex {
display: flex;
align-items: center;
padding: .5rem;
border: 1px solid #ffffff20;
border-radius: .5rem;
.secret {
overflow: auto;
}
}
@@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { AppSecretDialogComponent } from './app-secret-dialog.component';
describe('AppSecretDialogComponent', () => {
let component: AppSecretDialogComponent;
let fixture: ComponentFixture<AppSecretDialogComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [AppSecretDialogComponent],
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(AppSecretDialogComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,35 @@
import { Component, Inject } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
@Component({
selector: 'app-app-secret-dialog',
templateUrl: './app-secret-dialog.component.html',
styleUrls: ['./app-secret-dialog.component.scss'],
})
export class AppSecretDialogComponent {
public copied: string = '';
constructor(public dialogRef: MatDialogRef<AppSecretDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: any) { }
public closeDialog(): void {
this.dialogRef.close(false);
}
public copytoclipboard(value: string): void {
const selBox = document.createElement('textarea');
selBox.style.position = 'fixed';
selBox.style.left = '0';
selBox.style.top = '0';
selBox.style.opacity = '0';
selBox.value = value;
document.body.appendChild(selBox);
selBox.focus();
selBox.select();
document.execCommand('copy');
document.body.removeChild(selBox);
this.copied = value;
setTimeout(() => {
this.copied = '';
}, 3000);
}
}
@@ -0,0 +1,24 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { AppCreateComponent } from '../apps/app-create/app-create.component';
import { AppDetailComponent } from '../apps/app-detail/app-detail.component';
const routes: Routes = [
{
path: 'create',
component: AppCreateComponent,
data: { animation: 'AddPage' },
},
{
path: ':id',
component: AppDetailComponent,
data: { animation: 'HomePage' },
},
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class AppsRoutingModule { }
@@ -0,0 +1,58 @@
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule, NO_ERRORS_SCHEMA } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatButtonToggleModule } from '@angular/material/button-toggle';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatChipsModule } from '@angular/material/chips';
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 { MatMenuModule } from '@angular/material/menu';
import { MatProgressBarModule } from '@angular/material/progress-bar';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatSelectModule } from '@angular/material/select';
import { MatTooltipModule } from '@angular/material/tooltip';
import { TranslateModule } from '@ngx-translate/core';
import { HasRoleModule } from 'src/app/directives/has-role/has-role.module';
import { CardModule } from 'src/app/modules/card/card.module';
import { AppCreateComponent } from './app-create/app-create.component';
import { AppDetailComponent } from './app-detail/app-detail.component';
import { AppSecretDialogComponent } from './app-secret-dialog/app-secret-dialog.component';
import { AppsRoutingModule } from './apps-routing.module';
@NgModule({
declarations: [
AppCreateComponent,
AppDetailComponent,
AppSecretDialogComponent,
],
imports: [
CommonModule,
AppsRoutingModule,
FormsModule,
TranslateModule,
ReactiveFormsModule,
HasRoleModule,
MatFormFieldModule,
MatInputModule,
MatMenuModule,
MatChipsModule,
MatIconModule,
MatSelectModule,
MatButtonToggleModule,
MatButtonModule,
MatProgressSpinnerModule,
MatProgressBarModule,
MatDialogModule,
MatCheckboxModule,
CardModule,
MatTooltipModule,
TranslateModule,
],
exports: [TranslateModule],
schemas: [CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA],
})
export class AppsModule { }
@@ -0,0 +1,46 @@
<app-meta-layout>
<div class="max-width-container">
<div class="head" *ngIf="project?.id">
<a [routerLink]="[ '/granted-projects' ]" mat-icon-button>
<mat-icon class="icon">arrow_back</mat-icon>
</a>
<h1>{{ 'PROJECT.PAGES.TITLE' | translate }} {{project?.projectName}}</h1>
<div class="full-width">
<p class="desc">{{ 'PROJECT.PAGES.DESCRIPTION' | translate }}</p>
<p *ngIf="isZitadel" class="zitadel-warning">{{'PROJECT.PAGES.ZITADELPROJECT' | translate}}</p>
</div>
</div>
<!-- <ng-template appHasRole [appHasRole]="['project.grant.user.grant.read']"> -->
<app-card *ngIf="project?.projectId" title="{{ 'GRANTS.PROJECT.TITLE' | translate }}"
description="{{'GRANTS.PROJECT.DESCRIPTION' | translate }}">
<app-user-grants [context]="userGrantContext" [projectId]="projectId" [grantId]="grantId"
[allowCreate]="['project.grant.user.grant.write'] | hasRole"
[allowDelete]="['project.grant.user.grant.delete'] | hasRole">
</app-user-grants>
</app-card>
<!-- </ng-template> -->
</div>
<metainfo class="side">
<div class="details">
<div class="row">
<span class="first">{{'PROJECT.STATE.TITLE' | translate}}:</span>
<span *ngIf="project && project.state !== undefined"
class="second">{{'PROJECT.STATE.'+project.state | translate}}</span>
</div>
</div>
<mat-tab-group mat-stretch-tabs class="tab-group" disablePagination="true">
<mat-tab label="Details">
<app-project-contributors *ngIf="project && grantId"
[disabled]="project?.state !== ProjectState.PROJECTSTATE_ACTIVE"
[projectType]="ProjectType.PROJECTTYPE_GRANTED" [project]="project" [grantId]="grantId">
</app-project-contributors>
</mat-tab>
<mat-tab label="{{ 'CHANGES.PROJECT.TITLE' | translate }}" class="flex-col">
<app-changes *ngIf="project" [changeType]="ChangeType.PROJECT" [id]="project.id"></app-changes>
</mat-tab>
</mat-tab-group>
</metainfo>
</app-meta-layout>
@@ -0,0 +1,131 @@
.head {
display: flex;
align-items: center;
border-bottom: 1px solid #ffffff20;
flex-wrap: wrap;
margin-bottom: 1rem;
a {
display: block;
}
h1 {
font-size: 1.2rem;
margin: 0 1rem;
margin-left: 2rem;
font-weight: normal;
}
.full-width {
padding-top: 1rem;
width: 100%;
display: block;
.icon-button {
margin-right: 1rem;
}
button {
border-radius: .5rem;
}
.desc {
font-size: .9rem;
color: #8795a1;
}
.zitadel-warning {
font-size: 14px;
color: rgb(201,51,71);
}
}
}
.content {
display: flex;
flex-direction: row;
flex-wrap: wrap;
margin: 0 -.5rem;
mat-form-field {
flex: 1 1 33%;
margin: 0 .5rem;
}
}
h1 {
font-size: 1.2rem;
}
.desc {
width: 100%;
display: block;
font-size: .9rem;
color: #8795a1;
}
.side {
.details {
margin-bottom: 1rem;
border-bottom: 1px solid #81868a40;
padding-bottom: 1rem;
.row {
display: flex;
margin-bottom: 0.5rem;
align-items: center;
button {
display: none;
visibility: hidden;
}
&:hover {
button {
display: inline-block;
visibility: visible;
mat-icon {
font-size: 1.2rem;
}
}
}
.first {
flex: 1;
font-size: 0.8rem;
margin-right: 0.5rem;
}
.fill-space {
flex: 1;
}
.second {
font-size: 0.8rem;
text-overflow: ellipsis;
overflow: hidden;
margin-left: 1rem;
}
a {
&:hover {
cursor: pointer;
text-decoration: underline;
}
}
}
.side-section {
color: #8795a1;
}
}
.mat-tab-label {
min-width: 100px !important;
}
.flex-col {
display: flex;
flex-direction: column;
}
}
@@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { GrantedProjectDetailComponent } from './granted-project-detail.component';
describe('GrantedProjectDetailComponent', () => {
let component: GrantedProjectDetailComponent;
let fixture: ComponentFixture<GrantedProjectDetailComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [GrantedProjectDetailComponent],
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(GrantedProjectDetailComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,104 @@
import { SelectionModel } from '@angular/cdk/collections';
import { Location } from '@angular/common';
import { Component, OnDestroy, OnInit } from '@angular/core';
import { MatTableDataSource } from '@angular/material/table';
import { ActivatedRoute, Params } from '@angular/router';
import { TranslateService } from '@ngx-translate/core';
import { Subscription } from 'rxjs';
import { ChangeType } from 'src/app/modules/changes/changes.component';
import { UserGrantContext } from 'src/app/modules/user-grants/user-grants-datasource';
import {
Application,
ApplicationSearchResponse,
ProjectGrantView,
ProjectMember,
ProjectMemberSearchResponse,
ProjectRole,
ProjectRoleSearchResponse,
ProjectState,
ProjectType,
UserGrantSearchKey,
} from 'src/app/proto/generated/management_pb';
import { OrgService } from 'src/app/services/org.service';
import { ProjectService } from 'src/app/services/project.service';
import { ToastService } from 'src/app/services/toast.service';
@Component({
selector: 'app-granted-project-detail',
templateUrl: './granted-project-detail.component.html',
styleUrls: ['./granted-project-detail.component.scss'],
})
export class GrantedProjectDetailComponent implements OnInit, OnDestroy {
public projectId: string = '';
public grantId: string = '';
public project!: ProjectGrantView.AsObject;
public pageSizeRoles: number = 10;
public roleDataSource: MatTableDataSource<ProjectRole.AsObject> = new MatTableDataSource<ProjectRole.AsObject>();
public roleResult!: ProjectRoleSearchResponse.AsObject;
public roleColumns: string[] = ['name', 'displayname', 'group', 'actions'];
public pageSizeMembers: number = 10;
public projectDataSource: MatTableDataSource<ProjectMember.AsObject> = new MatTableDataSource<ProjectMember.AsObject>();
public memberResult!: ProjectMemberSearchResponse.AsObject;
public memberColumns: string[] = ['firstname', 'lastname', 'username', 'email', 'roles'];
public selection: SelectionModel<ProjectMember.AsObject> = new SelectionModel<ProjectMember.AsObject>(true, []);
public pageSizeApps: number = 10;
public appsDataSource: MatTableDataSource<Application.AsObject> = new MatTableDataSource<Application.AsObject>();
public appsResult!: ApplicationSearchResponse.AsObject;
public appsColumns: string[] = ['name'];
public ProjectState: any = ProjectState;
public ProjectType: any = ProjectType;
public ChangeType: any = ChangeType;
public grid: boolean = true;
private subscription?: Subscription;
public editstate: boolean = false;
public isZitadel: boolean = false;
public userGrantContext: UserGrantContext = UserGrantContext.GRANTED_PROJECT;
public userGrantSearchKey: UserGrantSearchKey = UserGrantSearchKey.USERGRANTSEARCHKEY_PROJECT_ID;
constructor(
public translate: TranslateService,
private route: ActivatedRoute,
private toast: ToastService,
private projectService: ProjectService,
private _location: Location,
private orgService: OrgService,
) {
}
public ngOnInit(): void {
this.subscription = this.route.params.subscribe(params => this.getData(params));
}
public ngOnDestroy(): void {
this.subscription?.unsubscribe();
}
private async getData({ id, grantId }: Params): Promise<void> {
this.projectId = id;
this.grantId = grantId;
this.orgService.GetIam().then(iam => {
this.isZitadel = iam.toObject().iamProjectId === this.projectId;
});
if (this.projectId && this.grantId) {
this.projectService.GetGrantedProjectByID(this.projectId, this.grantId).then(proj => {
this.project = proj.toObject();
console.log('granted-project', this.project);
}).catch(error => {
this.toast.showError(error);
});
}
}
public navigateBack(): void {
this._location.back();
}
}
@@ -0,0 +1,60 @@
<div class="view-toggle">
<button (click)="changedView.emit(true)" mat-icon-button>
<i class="show list view las la-th-list"></i>
</button>
</div>
<div class="container">
<mat-progress-bar *ngIf="loading" class="spinner" color="accent" mode="indeterminate"></mat-progress-bar>
<p class="n-items" *ngIf="!loading && selection.selected.length > 0">{{'PROJECT.PAGES.PINNED' | translate}}</p>
<div class="item card" *ngFor="let item of selection.selected; index as i"
[ngClass]="{ inactive: item.state !== ProjectState.PROJECTSTATE_ACTIVE}"
(click)="navigateToProject(item.projectId,item.id, $event)">
<div class="text-part">
<span *ngIf="item.changeDate" class="top">{{'PROJECT.PAGES.LASTMODIFIED' | translate}}
{{
item.changeDate | timestampToDate | date: 'EEE dd. MMM, HH:mm'
}}</span>
<span class="name" *ngIf="item.projectName">{{ item.projectName }}</span>
<span class="description" *ngIf="item.resourceOwnerName">{{item.resourceOwnerName}}</span>
<span *ngIf="item.changeDate" class="created">{{'PROJECT.PAGES.CREATEDON' | translate}}
{{ item.creationDate | timestampToDate | date: 'EEE dd. MMM, HH:mm' }}</span>
<span class="fill-space"></span>
<div class="icons">
</div>
</div>
<button [ngClass]="{ selected: selection.isSelected(item)}" (click)="selection.toggle(item)" class="edit-button"
mat-icon-button>
<mat-icon>push_pin_outline</mat-icon>
</button>
</div>
</div>
<div class="container">
<p class="n-items" *ngIf="!loading && notPinned.length > 0">{{'PROJECT.PAGES.ALL' | translate}}</p>
<div class="item card" *ngFor="let item of notPinned; index as i"
(click)="navigateToProject(item.projectId,item.id, $event)"
[ngClass]="{ inactive: item.state !== ProjectState.PROJECTSTATE_ACTIVE}">
<div class="text-part">
<span *ngIf="item.changeDate" class="top">{{'PROJECT.PAGES.LASTMODIFIED' | translate}}
{{
item.changeDate | timestampToDate | date: 'EEE dd. MMM, HH:mm'
}}</span>
<span class="name" *ngIf="item.projectName">{{ item.projectName }}</span>
<span class="description" *ngIf="item.resourceOwnerName">{{item.resourceOwnerName}}</span>
<span *ngIf="item.changeDate" class="created">{{'PROJECT.PAGES.CREATEDON' | translate}}
{{ item.creationDate | timestampToDate | date: 'EEE dd. MMM, HH:mm' }}</span>
<span class="fill-space"></span>
<div class="icons">
</div>
</div>
<button [ngClass]="{ selected: selection.isSelected(item)}" (click)="selection.toggle(item)" class="edit-button"
mat-icon-button>
<mat-icon>push_pin_outline</mat-icon>
</button>
</div>
<p class="n-items" *ngIf="!loading && items.length === 0 && selection.selected.length === 0">
{{'PROJECT.PAGES.NOITEMS' | translate}}</p>
</div>
@@ -0,0 +1,215 @@
.view-toggle {
width: 100%;
display: flex;
justify-content: flex-end;
padding-bottom: .5rem;
border-bottom: 1px solid #2d2e30;
.anim-list {
display: flex;
justify-content: flex-end;
align-items: center;
}
button {
&.left-button {
margin-right: 1rem;
}
}
}
.spinner {
margin: 1rem;
}
.container {
display: flex;
flex-wrap: wrap;
margin: 0 -1rem;
box-sizing: border-box;
h2 {
padding: 0 1rem;
flex-basis: 100%;
}
.item {
position: relative;
z-index: 100;
margin: 1rem;
flex-basis: 250px;
display: flex;
text-decoration: none;
cursor: pointer;
padding-top: 0;
padding-right: 0;
padding-bottom: 0;
padding-left: 1rem;
border-radius: 0.5rem;
box-sizing: border-box;
min-height: 166px;
&.inactive {
color: #8795a1;
}
img {
height: 50px;
width: 50px;
margin: 1rem;
}
.text-part {
flex: 1;
display: flex;
flex-direction: column;
min-height: 70px;
padding: 0.5rem 0;
.top {
font-size: 0.8rem;
margin-bottom: 0;
margin-top: .5rem;
color: #8795a1;
}
.name {
margin-top: 1rem;
font-size: 1.2rem;
margin-bottom: 0.5rem;
}
.description {
font-size: 0.8rem;
}
.created {
font-size: 0.8rem;
color: #8795a1;
}
.organization {
display: flex;
align-items: center;
margin-top: 1rem;
.org_avatar {
height: 25px;
width: 25px;
border-radius: 50%;
margin: 0;
margin-right: 1rem;
}
}
.fill-space {
flex: 1;
}
.icons {
margin-top: 1rem;
transition: all 0.3s;
opacity: 0;
.icon {
margin-right: 3px;
font-size: 1.3rem;
height: 1.4rem;
color: #8795a1;
}
}
span {
margin: 2px 0;
}
}
.edit-button {
opacity: 0;
user-select: none;
position: absolute;
bottom: 0;
right: 0;
margin: 0;
margin-bottom: 0.25rem;
color: #8795a1;
&:hover {
color: white;
}
&.selected {
opacity: 1;
}
}
&:hover {
.edit-button {
opacity: 1;
}
.text-part {
.icons {
opacity: 1;
}
}
}
&.selected {
.text-part {
.icons {
opacity: 1;
}
}
.edit-button {
opacity: 1;
}
.icon {
opacity: 1;
}
}
}
.add-project-button {
z-index: 100;
flex-basis: 250px;
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
min-height: 166px;
border-radius: 0.5rem;
margin: 1rem;
box-sizing: border-box;
.icon {
display: flex;
justify-content: center;
align-self: center;
margin-bottom: 1rem;
height: 2.5rem;
line-height: 2.5rem;
font-size: 2.5rem;
}
&:hover {
background-color: #ffffff25;
.icon,
span {
&.disabled {
color: gray;
}
}
}
}
}
.n-items {
flex-basis: 100%;
padding: 0 1rem;
font-size: .8rem;
color: #8795a1;
}
@@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { GrantedProjectGridComponent } from './granted-project-grid.component';
describe('GridComponent', () => {
let component: GrantedProjectGridComponent;
let fixture: ComponentFixture<GrantedProjectGridComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [GrantedProjectGridComponent],
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(GrantedProjectGridComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,107 @@
import { animate, animateChild, query, stagger, style, transition, trigger } from '@angular/animations';
import { SelectionModel } from '@angular/cdk/collections';
import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core';
import { Router } from '@angular/router';
import { ProjectGrantView, ProjectState, ProjectType } from 'src/app/proto/generated/management_pb';
import { AuthService } from 'src/app/services/auth.service';
@Component({
selector: 'app-granted-project-grid',
templateUrl: './granted-project-grid.component.html',
styleUrls: ['./granted-project-grid.component.scss'],
animations: [
trigger('list', [
transition(':enter', [
query('@animate',
stagger(100, animateChild()),
),
]),
]),
trigger('animate', [
transition(':enter', [
style({ opacity: 0, transform: 'translateY(-100%)' }),
animate('100ms', style({ opacity: 1, transform: 'translateY(0)' })),
]),
transition(':leave', [
style({ opacity: 1, transform: 'translateY(0)' }),
animate('100ms', style({ opacity: 0, transform: 'translateY(100%)' })),
]),
]),
],
})
export class GrantedProjectGridComponent implements OnChanges {
@Input() items: Array<ProjectGrantView.AsObject> = [];
public notPinned: Array<ProjectGrantView.AsObject> = [];
@Output() newClicked: EventEmitter<boolean> = new EventEmitter();
@Output() changedView: EventEmitter<boolean> = new EventEmitter();
@Input() loading: boolean = false;
public selection: SelectionModel<ProjectGrantView.AsObject> = new SelectionModel<ProjectGrantView.AsObject>(true, []);
public showNewProject: boolean = false;
public ProjectState: any = ProjectState;
public ProjectType: any = ProjectType;
constructor(private authService: AuthService, private router: Router) {
this.selection.changed.subscribe(selection => {
this.setPrefixedItem('pinned-granted-projects', JSON.stringify(
this.selection.selected.map(item => item.projectId),
)).then(() => {
const filtered = this.notPinned.filter(item => item === selection.added.find(i => i === item));
filtered.forEach((f, i) => {
this.notPinned.splice(i, 1);
});
this.notPinned.push(...selection.removed);
});
});
}
public addItem(): void {
this.newClicked.emit(true);
}
public ngOnChanges(changes: SimpleChanges): void {
if (changes.items?.currentValue && changes.items.currentValue.length > 0) {
this.notPinned = Object.assign([], this.items);
this.reorganizeItems();
}
}
public reorganizeItems(): void {
this.getPrefixedItem('pinned-granted-projects').then(storageEntry => {
if (storageEntry) {
const array: string[] = JSON.parse(storageEntry);
const toSelect: ProjectGrantView.AsObject[] = this.items.filter((item, index) => {
if (array.includes(item.projectId)) {
// this.notPinned.splice(index, 1);
return true;
}
});
this.selection.select(...toSelect);
const toNotPinned: ProjectGrantView.AsObject[] = this.items.filter((item, index) => {
if (!array.includes(item.projectId)) {
return true;
}
});
this.notPinned = toNotPinned;
}
});
}
private async getPrefixedItem(key: string): Promise<string | null> {
const prefix = (await this.authService.GetActiveOrg()).id;
return localStorage.getItem(`${prefix}:${key}`);
}
private async setPrefixedItem(key: string, value: any): Promise<void> {
const prefix = (await this.authService.GetActiveOrg()).id;
return localStorage.setItem(`${prefix}:${key}`, value);
}
public navigateToProject(projectId: string, id: string, event: any): void {
if (event && event.srcElement && event.srcElement.localName !== 'button') {
this.router.navigate(['/granted-projects', projectId, 'grant', id]);
}
}
}
@@ -0,0 +1,96 @@
<app-granted-project-grid *ngIf="grid" [loading]="loading$ | async" (changedView)="grid = false"
[items]="grantedProjectList">
</app-granted-project-grid>
<div *ngIf="!grid" class="view-toggle">
<button (click)="grid = true" mat-icon-button>
<i matTooltip="show grid view" class="las la-th-large"></i>
</button>
</div>
<div *ngIf="!grid && grantedProjectList">
<div class="table-header-row">
<div class="col">
<ng-container *ngIf="!selection.hasValue()">
<span class="desc">{{'ORG_DETAIL.TABLE.TOTAL' | translate}}</span>
<span class="count">{{dataSource?.data?.length}}</span>
</ng-container>
<ng-container *ngIf="selection.hasValue()">
<span class="desc">{{'ORG_DETAIL.TABLE.SELECTION' | translate}}</span>
<span class="count">{{selection?.selected?.length}}</span>
</ng-container>
</div>
<span class="fill-space"></span>
<div @list class="action-btns" *ngIf="selection.hasValue()">
<button @animate (click)="deactivateSelectedProjects()"
matTooltip="{{'PROJECT.TABLE.DEACTIVATE' | translate}}" class="icon-button" mat-icon-button>
<mat-icon svgIcon="mdi_light_off"></mat-icon>
</button>
<button @animate (click)="reactivateSelectedProjects()"
matTooltip="{{'PROJECT.TABLE.ACTIVATE' | translate}}" class="icon-button" mat-icon-button>
<mat-icon svgIcon="mdi_light_on"></mat-icon>
</button>
</div>
</div>
<div class="table-wrapper">
<div class="spinner-container" *ngIf="(loading$ | async) || (loading$ | async)">
<mat-spinner diameter="50"></mat-spinner>
</div>
<table class="background-style" mat-table [dataSource]="dataSource">
<ng-container matColumnDef="select">
<th class="selection" mat-header-cell *matHeaderCellDef>
<mat-checkbox color="primary" (change)="$event ? masterToggle() : null"
[checked]="selection.hasValue() && isAllSelected()"
[indeterminate]="selection.hasValue() && !isAllSelected()">
</mat-checkbox>
</th>
<td class="selection" mat-cell *matCellDef="let row">
<mat-checkbox color="primary" (click)="$event.stopPropagation()"
(change)="$event ? selection.toggle(row) : null" [checked]="selection.isSelected(row)">
</mat-checkbox>
</td>
</ng-container>
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.NAME' | translate }} </th>
<td mat-cell *matCellDef="let project"> {{project.projectName}} </td>
</ng-container>
<ng-container matColumnDef="resourceOwnerName">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.TABLE.RESOURCEOWNER' | translate }} </th>
<td class="pointer" mat-cell *matCellDef="let project">
{{project.resourceOwnerName}} </td>
</ng-container>
<ng-container matColumnDef="state">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.TABLE.STATE' | translate }} </th>
<td mat-cell *matCellDef="let project"><span
*ngIf="project.state">{{'PROJECT.STATE.'+project.state | translate}}</span></td>
</ng-container>
<ng-container matColumnDef="creationDate">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.TABLE.CREATIONDATE' | translate }} </th>
<td mat-cell *matCellDef="let project">
<span
*ngIf="project.creationDate">{{project.creationDate | timestampToDate | date: 'EEE dd. MMM, HH:mm'}}</span>
</td>
</ng-container>
<ng-container matColumnDef="changeDate">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.TABLE.CHANGEDATE' | translate }} </th>
<td mat-cell *matCellDef="let project">
<span
*ngIf="project.changeDate">{{project.changeDate | timestampToDate | date: 'EEE dd. MMM, HH:mm'}}</span>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr class="data-row" mat-row *matRowDef="let row; columns: displayedColumns;"
[routerLink]="['/granted-projects', row.id]"></tr>
</table>
<mat-paginator class="background-style" [length]="totalResult" [pageSize]="10" [pageSizeOptions]="[5, 10, 20]"
(page)="changePage($event)"></mat-paginator>
</div>
</div>
@@ -0,0 +1,94 @@
.content {
display: flex;
flex-direction: row;
flex-wrap: wrap;
margin: 0 -.5rem;
mat-form-field {
flex: 1 1 33%;
margin: 0 .5rem;
}
}
.view-toggle {
width: 100%;
display: flex;
justify-content: flex-end;
button {
display: block;
border-radius: .5rem;
}
}
.table-header-row {
display: flex;
align-items: center;
.col {
display: flex;
flex-direction: column;
.desc {
font-size: .8rem;
color: #8795a1;
}
.count {
font-size: 2rem;
}
}
.fill-space {
flex: 1;
}
.action-btns {
display: flex;
align-items: center;
}
.icon-button {
margin-right: .5rem;
}
}
.table-wrapper {
overflow: auto;
.spinner-container {
display: flex;
align-items: center;
justify-content: center;
}
table, mat-paginator {
width: 100%;
background-color: #2d2e30;
td, th {
&:first-child {
padding-left: 0;
padding-right: 1rem;
}
&:last-child {
padding-right: 0;
}
}
.data-row {
cursor: pointer;
&:hover {
background-color: #ffffff05;
}
}
.selection {
width: 50px;
max-width: 50px;
}
}
}
tr {
outline: none;
}
@@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { GrantedProjectListComponent } from './granted-project-list.component';
describe('ProjectListComponent', () => {
let component: GrantedProjectListComponent;
let fixture: ComponentFixture<GrantedProjectListComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [GrantedProjectListComponent],
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(GrantedProjectListComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,127 @@
import { animate, animateChild, query, stagger, style, transition, trigger } from '@angular/animations';
import { SelectionModel } from '@angular/cdk/collections';
import { Component, OnDestroy, OnInit } from '@angular/core';
import { PageEvent } from '@angular/material/paginator';
import { MatTableDataSource } from '@angular/material/table';
import { Router } from '@angular/router';
import { TranslateService } from '@ngx-translate/core';
import { BehaviorSubject, Observable, Subscription } from 'rxjs';
import { ProjectGrantView } from 'src/app/proto/generated/management_pb';
import { ProjectService } from 'src/app/services/project.service';
import { ToastService } from 'src/app/services/toast.service';
@Component({
selector: 'app-granted-project-list',
templateUrl: './granted-project-list.component.html',
styleUrls: ['./granted-project-list.component.scss'],
animations: [
trigger('list', [
transition(':enter', [
query('@animate',
stagger(80, animateChild()),
),
]),
]),
trigger('animate', [
transition(':enter', [
style({ opacity: 0, transform: 'translateY(-100%)' }),
animate('100ms', style({ opacity: 1, transform: 'translateY(0)' })),
]),
transition(':leave', [
style({ opacity: 1, transform: 'translateY(0)' }),
animate('100ms', style({ opacity: 0, transform: 'translateY(100%)' })),
]),
]),
],
})
export class GrantedProjectListComponent implements OnInit, OnDestroy {
public totalResult: number = 0;
public dataSource: MatTableDataSource<ProjectGrantView.AsObject> =
new MatTableDataSource<ProjectGrantView.AsObject>();
public grantedProjectList: ProjectGrantView.AsObject[] = [];
public displayedColumns: string[] = ['select', 'name', 'resourceOwnerName', 'state', 'creationDate', 'changeDate'];
public selection: SelectionModel<ProjectGrantView.AsObject> = new SelectionModel<ProjectGrantView.AsObject>(true, []);
private loadingSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
public loading$: Observable<boolean> = this.loadingSubject.asObservable();
public grid: boolean = true;
private subscription?: Subscription;
constructor(private router: Router,
public translate: TranslateService,
private projectService: ProjectService,
private toast: ToastService,
) { }
public ngOnInit(): void {
this.getData(10, 0);
}
public ngOnDestroy(): void {
this.subscription?.unsubscribe();
}
public isAllSelected(): boolean {
const numSelected = this.selection.selected.length;
const numRows = this.dataSource.data.length;
return numSelected === numRows;
}
public masterToggle(): void {
this.isAllSelected() ?
this.selection.clear() :
this.dataSource.data.forEach(row => this.selection.select(row));
}
public changePage(event: PageEvent): void {
this.getData(event.pageSize, event.pageIndex);
}
public addProject(): void {
this.router.navigate(['/projects', 'create']);
}
private async getData(limit: number, offset: number): Promise<void> {
this.loadingSubject.next(true);
this.projectService.SearchGrantedProjects(limit, offset).then(res => {
this.grantedProjectList = res.toObject().resultList;
this.totalResult = res.toObject().totalResult;
if (this.totalResult > 5) {
this.grid = false;
}
this.dataSource.data = this.grantedProjectList;
this.loadingSubject.next(false);
}).catch(error => {
console.error(error);
this.toast.showError(error);
this.loadingSubject.next(false);
});
}
public reactivateSelectedProjects(): void {
const promises = this.selection.selected.map(project => {
this.projectService.ReactivateProject(project.id);
});
Promise.all(promises).then(() => {
this.toast.showInfo('Reactivated selected projects successfully');
}).catch(error => {
this.toast.showInfo(error.message);
});
}
public deactivateSelectedProjects(): void {
const promises = this.selection.selected.map(project => {
this.projectService.DeactivateProject(project.id);
});
Promise.all(promises).then(() => {
this.toast.showInfo('Deactivated selected projects Successfully');
}).catch(error => {
this.toast.showInfo(error.message);
});
}
}
@@ -0,0 +1,53 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { AuthGuard } from 'src/app/guards/auth.guard';
import { RoleGuard } from 'src/app/guards/role.guard';
import { ProjectType } from 'src/app/proto/generated/management_pb';
import { GrantedProjectDetailComponent } from './granted-project-detail/granted-project-detail.component';
import { GrantedProjectsComponent } from './granted-projects.component';
const routes: Routes = [
{
path: '',
component: GrantedProjectsComponent,
data: { animation: 'HomePage' },
},
{
path: 'create',
loadChildren: () => import('../project-create/project-create.module').then(m => m.ProjectCreateModule),
canActivate: [AuthGuard, RoleGuard],
data: {
roles: ['project.write'],
},
},
{
path: ':projectid/grant/:grantid/members',
data: {
type: ProjectType.PROJECTTYPE_GRANTED,
},
loadChildren: () => import('src/app/modules/project-members/project-members.module')
.then(m => m.ProjectMembersModule),
},
{
path: ':id/grant/:grantId',
component: GrantedProjectDetailComponent,
data: { animation: 'HomePage' },
},
{
path: ':projectid/roles/create',
loadChildren: () => import('../project-role-create/project-role-create.module').then(m => m.ProjectRoleCreateModule),
},
{
path: ':projectid/grants/create',
loadChildren: () => import('../project-grant-create/project-grant-create.module')
.then(m => m.ProjectGrantCreateModule),
},
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class GrantedProjectsRoutingModule { }
@@ -0,0 +1,8 @@
<div class="max-width-container">
<h1>{{ 'PROJECT.PAGES.LIST' | translate }}</h1>
<p class="sub">{{ 'PROJECT.PAGES.LISTDESCRIPTION' | translate }}</p>
<h2>{{'PROJECT.PAGES.TYPE.GRANTED' | translate}}</h2>
<app-granted-project-list>
</app-granted-project-list>
</div>
@@ -0,0 +1,12 @@
h1 {
margin-top: 0;
}
.sub {
color: #8795a1;
margin-bottom: 2rem;
}
.max-width-container {
padding-bottom: 3rem;
}
@@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { GrantedProjectsComponent } from './granted-projects.component';
describe('GrantedProjectsComponent', () => {
let component: GrantedProjectsComponent;
let fixture: ComponentFixture<GrantedProjectsComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [GrantedProjectsComponent],
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(GrantedProjectsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,10 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-granted-projects',
templateUrl: './granted-projects.component.html',
styleUrls: ['./granted-projects.component.scss'],
})
export class GrantedProjectsComponent {
constructor() { }
}
@@ -0,0 +1,71 @@
import { CommonModule } from '@angular/common';
import { NgModule, NO_ERRORS_SCHEMA } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatPaginatorModule } from '@angular/material/paginator';
import { MatProgressBarModule } from '@angular/material/progress-bar';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatSelectModule } from '@angular/material/select';
import { MatSortModule } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table';
import { MatTabsModule } from '@angular/material/tabs';
import { MatTooltipModule } from '@angular/material/tooltip';
import { TranslateModule } from '@ngx-translate/core';
import { HasRoleModule } from 'src/app/directives/has-role/has-role.module';
import { CardModule } from 'src/app/modules/card/card.module';
import { ChangesModule } from 'src/app/modules/changes/changes.module';
import { MetaLayoutModule } from 'src/app/modules/meta-layout/meta-layout.module';
import { ProjectContributorsModule } from 'src/app/modules/project-contributors/project-contributors.module';
import { ProjectRolesModule } from 'src/app/modules/project-roles/project-roles.module';
import { UserGrantsModule } from 'src/app/modules/user-grants/user-grants.module';
import { PipesModule } from 'src/app/pipes/pipes.module';
import { GrantedProjectDetailComponent } from './granted-project-detail/granted-project-detail.component';
import { GrantedProjectGridComponent } from './granted-project-grid/granted-project-grid.component';
import { GrantedProjectListComponent } from './granted-project-list/granted-project-list.component';
import { GrantedProjectsRoutingModule } from './granted-projects-routing.module';
import { GrantedProjectsComponent } from './granted-projects.component';
@NgModule({
declarations: [
GrantedProjectsComponent,
GrantedProjectListComponent,
GrantedProjectGridComponent,
GrantedProjectDetailComponent,
],
imports: [
CommonModule,
UserGrantsModule,
GrantedProjectsRoutingModule,
ProjectContributorsModule,
FormsModule,
TranslateModule,
ReactiveFormsModule,
HasRoleModule,
MatTableModule,
MatPaginatorModule,
MatFormFieldModule,
MatInputModule,
ChangesModule,
MatIconModule,
MatSelectModule,
MatButtonModule,
MatProgressSpinnerModule,
MetaLayoutModule,
MatProgressBarModule,
MatTabsModule,
ProjectRolesModule,
MatCheckboxModule,
CardModule,
MatTooltipModule,
MatSortModule,
PipesModule,
TranslateModule,
],
schemas: [NO_ERRORS_SCHEMA],
})
export class GrantedProjectsModule { }
@@ -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>
@@ -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;
}
}
@@ -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();
});
});
@@ -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;
}
}
@@ -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>
@@ -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%;
}
@@ -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();
});
});
@@ -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);
}
}
@@ -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>
@@ -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;
}
@@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { GrantedProjectListComponent } from './granted-project-list.component';
describe('ProjectListComponent', () => {
let component: GrantedProjectListComponent;
let fixture: ComponentFixture<GrantedProjectListComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [GrantedProjectListComponent],
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(GrantedProjectListComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,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 { }
@@ -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>
@@ -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;
}
}
}
@@ -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();
});
});
@@ -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();
}
}
@@ -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();
}
}
@@ -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>
@@ -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;
}
@@ -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();
});
});
@@ -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));
}
}
@@ -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();
}
}
@@ -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 { }
@@ -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>
@@ -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;
}
}
}
@@ -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();
});
});
@@ -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);
});
}
}
@@ -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 { }
@@ -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>
@@ -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;
}
}
@@ -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();
});
});
@@ -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);
}
}
@@ -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 { }
@@ -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();
}
}
@@ -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>
@@ -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;
}
@@ -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();
});
});
@@ -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);
});
}
}
@@ -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 { }
@@ -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();
}
}
@@ -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>
@@ -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;
}
@@ -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();
});
});
@@ -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);
});
}
}
@@ -0,0 +1,18 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { ProjectCreateComponent } from './project-create.component';
const routes: Routes = [
{
path: '',
component: ProjectCreateComponent,
data: { animation: 'DetailPage' },
},
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class ProjectCreateRoutingModule { }
@@ -0,0 +1,25 @@
<div class="container">
<div class="abort-container">
<button (click)="close()" mat-icon-button>
<mat-icon>close</mat-icon>
</button>
<span class="abort">{{ 'PROJECT.PAGES.CREATE' | translate }}</span><span class="abort-2">Step
{{ currentCreateStep }} of
{{ createSteps }}</span>
</div>
<h1>{{'PROJECT.PAGES.CREATE_DESC' | translate}}</h1>
<form (ngSubmit)="saveProject()">
<div class="column">
<mat-form-field class="formfield" hintLabel="The name is required!">
<mat-label>{{'PROJECT.NAME' | translate}}</mat-label>
<input matInput autofocus [(ngModel)]="project.name" [ngModelOptions]="{ standalone: true }" />
</mat-form-field>
</div>
<button color="primary" mat-raised-button class="continue-button" [disabled]="!project.name" cdkFocusInitial
type="submit">
{{'CONTINUE' | translate}}
</button>
</form>
</div>
@@ -0,0 +1,61 @@
h1 {
font-weight: 500;
}
.container {
padding: 4rem 4rem 2rem 4rem;
.abort-container {
display: flex;
align-items: center;
margin-bottom: 2rem;
.abort {
font-size: 1.2rem;
margin-left: 2rem;
}
.abort-2 {
font-size: 1.2rem;
margin-left: 2rem;
}
}
}
.margin-right {
margin-right: 0.5rem;
}
.column {
display: flex;
flex-direction: column;
.formfield {
width: 400px;
input {
font-size: 1.5rem;
}
&.autocomplete {
margin-top: 1rem;
}
}
.slider {
margin-top: 2rem;
margin-bottom: 1rem;
font-size: 0.8rem;
}
.access-token {
width: 400px;
}
}
.continue-button {
margin-top: 3rem;
display: block;
padding: 0.5rem 4rem;
border-radius: 0.5rem;
}
@@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ProjectCreateComponent } from './project-create.component';
describe('ProjectCreateComponent', () => {
let component: ProjectCreateComponent;
let fixture: ComponentFixture<ProjectCreateComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ProjectCreateComponent],
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ProjectCreateComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,41 @@
import { Location } from '@angular/common';
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { Project, ProjectCreateRequest } from 'src/app/proto/generated/management_pb';
import { ProjectService } from 'src/app/services/project.service';
import { ToastService } from 'src/app/services/toast.service';
@Component({
selector: 'app-project-create',
templateUrl: './project-create.component.html',
styleUrls: ['./project-create.component.scss'],
})
export class ProjectCreateComponent implements OnInit {
public project: ProjectCreateRequest.AsObject = new ProjectCreateRequest().toObject();
constructor(
private router: Router,
private toast: ToastService,
private projectService: ProjectService,
private _location: Location,
) { }
public createSteps: number = 1;
public currentCreateStep: number = 1;
public ngOnInit(): void { }
public saveProject(): void {
this.projectService
.CreateProject(this.project)
.then((data: Project) => {
this.router.navigate(['projects', data.getId()]);
})
.catch(data => {
this.toast.showError(data.message);
});
}
public close(): void {
this._location.back();
}
}
@@ -0,0 +1,26 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { TranslateModule } from '@ngx-translate/core';
import { ProjectCreateRoutingModule } from './project-create-routing.module';
import { ProjectCreateComponent } from './project-create.component';
@NgModule({
declarations: [ProjectCreateComponent],
imports: [
ProjectCreateRoutingModule,
CommonModule,
FormsModule,
MatInputModule,
MatFormFieldModule,
MatButtonModule,
MatIconModule,
TranslateModule,
],
})
export class ProjectCreateModule { }
@@ -0,0 +1,18 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { ProjectGrantCreateComponent } from './project-grant-create.component';
const routes: Routes = [
{
path: '',
component: ProjectGrantCreateComponent,
data: { animation: 'AddPage' },
},
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class ProjectGrantCreateRoutingModule { }
@@ -0,0 +1,58 @@
<div class="max-width-container container">
<div class="abort-container">
<button (click)="close()" mat-icon-button>
<mat-icon>close</mat-icon>
</button>
<span class="abort">{{ 'PROJECT.GRANT.CREATE.TITLE' | translate }}</span><span class="abort-2">Step
{{ currentCreateStep }} of
{{ STEPS }}</span>
</div>
<ng-container *ngIf="currentCreateStep === 1">
<h1>{{'PROJECT.GRANT.CREATE.SEL_ORG' | translate}}</h1>
<p>{{'PROJECT.GRANT.CREATE.SEL_ORG_DESC' | translate}}</p>
<form (ngSubmit)="searchOrg(domain.value)">
<mat-form-field class="org-domain">
<mat-label>{{'PROJECT.GRANT.CREATE.SEL_ORG_FORMFIELD' | translate}}</mat-label>
<input #domain matInput />
</mat-form-field>
<button [disabled]="domain.value.length == 0" color="primary" type="submit" class="domain-button"
mat-raised-button>
{{'PROJECT.GRANT.CREATE.SEL_ORG_BUTTON' | translate}}
</button>
</form>
<span *ngIf="org">
{{'PROJECT.GRANT.CREATE.FOR_ORG' | translate}} {{org?.name}}
</span>
</ng-container>
<ng-container *ngIf="currentCreateStep === 2">
<h1>{{'PROJECT.GRANT.CREATE.SEL_ROLES' | translate}}</h1>
<app-card *ngIf="projectId">
<app-project-roles (changedSelection)="selectRoles($event)" [projectId]="projectId"></app-project-roles>
</app-card>
</ng-container>
<div class="btn-container">
<ng-container *ngIf="currentCreateStep === 1">
<button [disabled]="!org" (click)="next()" color="primary" mat-raised-button class="big-button"
cdkFocusInitial>
{{ 'ACTIONS.CONTINUE' | translate }}
</button>
</ng-container>
<ng-container *ngIf="currentCreateStep === STEPS">
<button (click)="previous()" color="primary" mat-button class="small-button">
{{ 'ACTIONS.BACK' | translate }}
</button>
<button color="primary" [disabled]="!org" (click)="addGrant()" mat-raised-button class="big-button"
cdkFocusInitial>
{{ 'ACTIONS.SAVE' | translate }}
</button>
</ng-container>
</div>
</div>
@@ -0,0 +1,108 @@
.container {
padding: 4rem 4rem 2rem 4rem;
.abort-container {
display: flex;
align-items: center;
margin-bottom: 2rem;
white-space: nowrap;
.abort {
font-size: 1.2rem;
margin-left: 2rem;
}
.abort-2 {
font-size: 1.2rem;
margin-left: 2rem;
}
}
}
.org-domain {
width: 400px;
margin-right: 1rem;
}
.domain-button {
margin-right: 1rem;
margin-bottom: 1rem;
border-radius: .5rem;
}
.org {
height: 120px;
padding: 1rem;
display: flex;
justify-content: center;
flex-direction: column;
.name {
margin-top: 0;
font-size: 1rem;
margin-bottom: 1rem;
}
span {
font-size: .8rem;
}
}
.margin-right {
margin-right: 0.5rem;
}
.column {
display: flex;
flex-direction: column;
.formfield {
width: 400px;
input {
font-size: 1.5rem;
}
&.autocomplete {
margin-top: 1rem;
}
}
.slider {
margin-top: 2rem;
margin-bottom: 1rem;
font-size: 0.8rem;
}
.access-token {
width: 400px;
}
}
.content {
display: flex;
margin: 0 -.5rem;
flex-wrap: wrap;
mat-form-field {
flex: 1 0 40%;
margin: 0 .5rem;
}
}
.btn-container {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 3rem;
.small-button {
display: block;
border-radius: 0.5rem;
}
.big-button {
display: block;
padding: 0.5rem 4rem;
border-radius: 0.5rem;
}
}
@@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ProjectGrantCreateComponent } from './project-grant-create.component';
describe('GrantCreateComponent', () => {
let component: ProjectGrantCreateComponent;
let fixture: ComponentFixture<ProjectGrantCreateComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ProjectGrantCreateComponent],
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ProjectGrantCreateComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,86 @@
import { Location } from '@angular/common';
import { Component, OnDestroy, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Subscription } from 'rxjs';
import { Org, ProjectRole } from 'src/app/proto/generated/management_pb';
import { AuthService } from 'src/app/services/auth.service';
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-create',
templateUrl: './project-grant-create.component.html',
styleUrls: ['./project-grant-create.component.scss'],
})
export class ProjectGrantCreateComponent implements OnInit, OnDestroy {
public org!: Org.AsObject;
public projectId: string = '';
public grantId: string = '';
public rolesKeyList: string[] = [];
public STEPS: number = 2;
public currentCreateStep: number = 1;
private routeSubscription: Subscription = new Subscription();
constructor(
private orgService: OrgService,
private route: ActivatedRoute,
private toast: ToastService,
private projectService: ProjectService,
private authService: AuthService,
private _location: Location,
) { }
public ngOnInit(): void {
this.routeSubscription = this.route.params.subscribe(params => {
this.projectId = params.projectid;
});
}
public ngOnDestroy(): void {
this.routeSubscription.unsubscribe();
}
public searchOrg(domain: string): void {
this.orgService.getOrgByDomainGlobal(domain).then((ret) => {
const tmp = ret.toObject();
this.authService.GetActiveOrg().then((org) => {
if (tmp !== org) {
this.org = tmp;
}
});
this.org = ret.toObject();
}).catch(error => {
this.toast.showError(error);
});
}
public close(): void {
this._location.back();
}
public addGrant(): void {
this.projectService
.CreateProjectGrant(this.org.id, this.projectId, this.rolesKeyList)
.then((data) => {
this.close();
})
.catch(error => {
this.toast.showError(error);
});
}
public selectRoles(roles: ProjectRole.AsObject[]): void {
this.rolesKeyList = roles.map(role => role.key);
}
public next(): void {
this.currentCreateStep++;
}
public previous(): void {
this.currentCreateStep--;
}
}
@@ -0,0 +1,43 @@
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 { MatInputModule } from '@angular/material/input';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatTooltipModule } from '@angular/material/tooltip';
import { TranslateModule } from '@ngx-translate/core';
import { CardModule } from 'src/app/modules/card/card.module';
import { ProjectRolesModule } from 'src/app/modules/project-roles/project-roles.module';
import { PipesModule } from 'src/app/pipes/pipes.module';
import { ProjectGrantCreateRoutingModule } from './project-grant-create-routing.module';
import { ProjectGrantCreateComponent } from './project-grant-create.component';
@NgModule({
declarations: [ProjectGrantCreateComponent],
imports: [
ProjectGrantCreateRoutingModule,
CommonModule,
MatAutocompleteModule,
MatChipsModule,
MatButtonModule,
MatFormFieldModule,
MatInputModule,
CardModule,
MatCheckboxModule,
ProjectRolesModule,
MatIconModule,
MatTooltipModule,
PipesModule,
ReactiveFormsModule,
MatProgressSpinnerModule,
FormsModule,
TranslateModule,
],
})
export class ProjectGrantCreateModule { }
@@ -0,0 +1,18 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { ProjectRoleCreateComponent } from './project-role-create.component';
const routes: Routes = [
{
path: '',
component: ProjectRoleCreateComponent,
data: { animation: 'DetailPage' },
},
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class ProjectRoleCreateRoutingModule { }
@@ -0,0 +1,44 @@
<div class="container">
<div class="abort-container">
<button (click)="close()" mat-icon-button>
<mat-icon>close</mat-icon>
</button>
<span class="abort">{{ 'PROJECT.ROLE.ADDTITLE' | translate }}</span><span class="abort-2">Step
{{ currentCreateStep }} of
{{ createSteps }}</span>
</div>
<h1>{{'PROJECT.ROLE.ADDDESCRIPTION' | translate}}</h1>
<button class="add-line-btn" color="primary" mat-stroked-button (click)="addEntry()">
Add another role
</button>
<form @list (ngSubmit)="addRole()">
<div @animate *ngFor="let formGroup of formArray.controls; index as i" class="content">
<ng-container [formGroup]="formGroup">
<mat-form-field appearance="outline" class="formfield">
<mat-label>{{ 'PROJECT.ROLE.KEY' | translate }}</mat-label>
<input matInput formControlName="key" />
<!-- <mat-error *ngIf="formGroup.get('key')?.errors?.required">{{'ERRORS.REQUIRED' | translate}}
</mat-error> -->
</mat-form-field>
<mat-form-field appearance="outline" class="formfield">
<mat-label>{{ 'PROJECT.ROLE.DISPLAY_NAME' | translate }}</mat-label>
<input matInput formControlName="displayName" />
</mat-form-field>
<mat-form-field appearance="outline" class="formfield">
<mat-label>{{ 'PROJECT.ROLE.GROUP' | translate }}</mat-label>
<input matInput formControlName="group" />
</mat-form-field>
<button mat-icon-button (click)="removeEntry(i)" color="warn"
matTooltip="{{ 'ACTIONS.REMOVE' | translate }}">
<i class="las la-trash"></i>
</button>
</ng-container>
</div>
<button color="primary" mat-raised-button class="continue-button" [disabled]="formArray.invalid" cdkFocusInitial
type="submit">
{{ 'ACTIONS.SAVE' | translate }}
</button>
</form>
</div>
@@ -0,0 +1,47 @@
.container {
padding: 4rem 4rem 2rem 4rem;
.abort-container {
display: flex;
align-items: center;
margin-bottom: 2rem;
.abort {
font-size: 1.2rem;
margin-left: 2rem;
}
.abort-2 {
font-size: 1.2rem;
margin-left: 2rem;
}
}
.add-line-btn {
margin-bottom: 1rem;
border-radius: .5rem;
}
}
.margin-right {
margin-right: 0.5rem;
}
.content {
display: flex;
margin: 0 -.5rem;
flex-wrap: wrap;
// align-items: center;
mat-form-field {
margin: 0 .5rem;
}
}
.continue-button {
margin-top: 3rem;
display: block;
padding: 0.5rem 4rem;
border-radius: 0.5rem;
}
@@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ProjectRoleCreateComponent } from './project-role-create.component';
describe('ProjectRoleCreateComponent', () => {
let component: ProjectRoleCreateComponent;
let fixture: ComponentFixture<ProjectRoleCreateComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ProjectRoleCreateComponent],
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ProjectRoleCreateComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,106 @@
import { animate, animateChild, query, stagger, style, transition, trigger } from '@angular/animations';
import { Location } from '@angular/common';
import { Component, OnDestroy, OnInit } from '@angular/core';
import { FormArray, FormControl, FormGroup, Validators } from '@angular/forms';
import { ActivatedRoute, Params, Router } from '@angular/router';
import { Subscription } from 'rxjs';
import { ProjectRoleAdd } from 'src/app/proto/generated/management_pb';
import { ProjectService } from 'src/app/services/project.service';
import { ToastService } from 'src/app/services/toast.service';
@Component({
selector: 'app-project-role-create',
templateUrl: './project-role-create.component.html',
styleUrls: ['./project-role-create.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 ProjectRoleCreateComponent implements OnInit, OnDestroy {
private subscription?: Subscription;
public projectId: string = '';
public formArray!: FormArray;
public formGroup!: FormGroup;
public createSteps: number = 1;
public currentCreateStep: number = 1;
constructor(
private router: Router,
private route: ActivatedRoute,
private toast: ToastService,
private projectService: ProjectService,
private _location: Location,
) {
this.formGroup = new FormGroup({
key: new FormControl('', [Validators.required]),
displayName: new FormControl(''),
group: new FormControl(''),
});
this.formArray = new FormArray([this.formGroup]);
}
public addEntry(): void {
const newGroup = new FormGroup({
key: new FormControl('', [Validators.required]),
displayName: new FormControl(''),
group: new FormControl(''),
});
this.formArray.push(newGroup);
}
public removeEntry(index: number): void {
this.formArray.removeAt(index);
}
public ngOnInit(): void {
this.subscription = this.route.params.subscribe(params => this.getData(params));
}
public ngOnDestroy(): void {
this.subscription?.unsubscribe();
}
private getData({ projectid }: Params): void {
this.projectId = projectid;
}
public addRole(): void {
const rolesToAdd: ProjectRoleAdd[] = this.formArray.value.map((element: any) => {
const role = new ProjectRoleAdd();
role.setKey(element.key);
role.setDisplayName(element.displayName);
role.setGroup(element.group);
return role;
});
this.projectService.BulkAddProjectRole(this.projectId, rolesToAdd).then(() => {
this.router.navigate(['projects', this.projectId]);
}).catch(error => {
this.toast.showError(error);
});
}
public close(): void {
this._location.back();
}
}
@@ -0,0 +1,29 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatTooltipModule } from '@angular/material/tooltip';
import { TranslateModule } from '@ngx-translate/core';
import { ProjectRoleCreateRoutingModule } from './project-role-create-routing.module';
import { ProjectRoleCreateComponent } from './project-role-create.component';
@NgModule({
declarations: [ProjectRoleCreateComponent],
imports: [
ProjectRoleCreateRoutingModule,
CommonModule,
FormsModule,
ReactiveFormsModule,
MatInputModule,
MatFormFieldModule,
MatButtonModule,
MatIconModule,
MatTooltipModule,
TranslateModule,
],
})
export class ProjectRoleCreateModule { }