fix(console): refactoring (#197)

* return error from changes

* project member context, org-policies, state

* project type seperation

* chore(deps): bump grpc from 1.24.2 to 1.24.3 in /console (#183)

Bumps [grpc](https://github.com/grpc/grpc-node) from 1.24.2 to 1.24.3.
- [Release notes](https://github.com/grpc/grpc-node/releases)
- [Commits](https://github.com/grpc/grpc-node/compare/grpc@1.24.2...grpc@1.24.3)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* chore(deps): bump google-proto-files from 1.1.2 to 2.1.0 in /console (#176)

Bumps [google-proto-files](https://github.com/googleapis/nodejs-proto-files) from 1.1.2 to 2.1.0.
- [Release notes](https://github.com/googleapis/nodejs-proto-files/releases)
- [Changelog](https://github.com/googleapis/nodejs-proto-files/blob/master/CHANGELOG.md)
- [Commits](https://github.com/googleapis/nodejs-proto-files/compare/v1.1.2...v2.1.0)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* chore(deps-dev): bump karma-coverage-istanbul-reporter in /console (#169)

Bumps [karma-coverage-istanbul-reporter](https://github.com/mattlewis92/karma-coverage-istanbul-reporter) from 3.0.2 to 3.0.3.
- [Release notes](https://github.com/mattlewis92/karma-coverage-istanbul-reporter/releases)
- [Changelog](https://github.com/mattlewis92/karma-coverage-istanbul-reporter/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mattlewis92/karma-coverage-istanbul-reporter/compare/v3.0.2...v3.0.3)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* update packages

* update deps

* lint

* replace assets

* add key, creationdate for roles

* project grant members

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
This commit is contained in:
Max Peintner 2020-06-10 12:59:12 +02:00 committed by GitHub
parent e0fb19b4e9
commit 2d369fbcd3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
51 changed files with 1051 additions and 1376 deletions

1952
console/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -14,53 +14,53 @@
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
"@angular/animations": "~9.1.0", "@angular/animations": "~9.1.10",
"@angular/cdk": "~9.0.1", "@angular/cdk": "~9.2.4",
"@angular/common": "~9.1.0", "@angular/common": "~9.1.10",
"@angular/compiler": "~9.1.0", "@angular/compiler": "~9.1.10",
"@angular/core": "~9.1.0", "@angular/core": "~9.1.10",
"@angular/forms": "~9.1.0", "@angular/forms": "~9.1.10",
"@angular/material": "^9.0.1", "@angular/material": "^9.2.4",
"@angular/platform-browser": "~9.1.0", "@angular/platform-browser": "~9.1.10",
"@angular/platform-browser-dynamic": "~9.1.0", "@angular/platform-browser-dynamic": "~9.1.10",
"@angular/router": "~9.1.0", "@angular/router": "~9.1.10",
"@angular/service-worker": "~9.1.0", "@angular/service-worker": "~9.1.10",
"@ngx-translate/core": "^12.1.2", "@ngx-translate/core": "^12.1.2",
"@ngx-translate/http-loader": "^4.0.0", "@ngx-translate/http-loader": "^4.0.0",
"@types/google-protobuf": "^3.7.2", "@types/google-protobuf": "^3.7.2",
"@types/uuid": "^8.0.0", "@types/uuid": "^8.0.0",
"angular-oauth2-oidc": "^8.0.4", "angular-oauth2-oidc": "^9.2.2",
"angularx-qrcode": "^2.1.0", "angularx-qrcode": "^2.3.4",
"cors": "^2.8.5", "cors": "^2.8.5",
"google-proto-files": "^1.1.1", "google-proto-files": "^2.1.0",
"google-protobuf": "^3.12.0", "google-protobuf": "^3.12.2",
"grpc": "^1.24.2", "grpc": "^1.24.3",
"grpc-web": "^1.1.0", "grpc-web": "^1.1.0",
"hammerjs": "^2.0.8", "hammerjs": "^2.0.8",
"moment": "^2.24.0", "moment": "^2.26.0",
"ngx-moment": "^3.5.0", "ngx-moment": "^3.5.0",
"prettier-stylelint": "^0.4.2", "prettier-stylelint": "^0.4.2",
"rxjs": "~6.5.5", "rxjs": "~6.5.5",
"ts-protoc-gen": "^0.12.0", "ts-protoc-gen": "^0.12.0",
"tslib": "^1.13.0", "tslib": "^2.0.0",
"uuid": "^7.0.1", "uuid": "^8.1.0",
"zone.js": "~0.10.3" "zone.js": "~0.10.3"
}, },
"devDependencies": { "devDependencies": {
"@angular-devkit/build-angular": "~0.901.7", "@angular-devkit/build-angular": "~0.901.7",
"@angular/cli": "~9.1.0", "@angular/cli": "~9.1.7",
"@angular/compiler-cli": "~9.1.0", "@angular/compiler-cli": "~9.1.10",
"@angular/language-service": "~9.1.9", "@angular/language-service": "~9.1.10",
"@types/jasmine": "~3.5.10", "@types/jasmine": "~3.5.10",
"@types/jasminewd2": "~2.0.3", "@types/jasminewd2": "~2.0.3",
"@types/node": "^14.0.11", "@types/node": "^14.0.13",
"codelyzer": "^5.1.2", "codelyzer": "^5.2.2",
"jasmine-core": "~3.5.0", "jasmine-core": "~3.5.0",
"karma": "^5.0.9",
"jasmine-spec-reporter": "~5.0.2", "jasmine-spec-reporter": "~5.0.2",
"karma": "^5.0.9",
"karma-chrome-launcher": "~3.1.0", "karma-chrome-launcher": "~3.1.0",
"karma-coverage-istanbul-reporter": "~3.0.2", "karma-coverage-istanbul-reporter": "~3.0.3",
"karma-jasmine": "~3.1.1", "karma-jasmine": "^3.3.1",
"karma-jasmine-html-reporter": "^1.5.4", "karma-jasmine-html-reporter": "^1.5.4",
"prettier": "^2.0.5", "prettier": "^2.0.5",
"protractor": "^7.0.0", "protractor": "^7.0.0",

View File

@ -39,10 +39,11 @@
<div (clickOutside)="closeAccountCard()" class="icon-container"> <div (clickOutside)="closeAccountCard()" class="icon-container">
<div class="avatar-wrapper dontcloseonclick" (click)="showAccount = !showAccount"> <div class="avatar-wrapper dontcloseonclick" (click)="showAccount = !showAccount">
<div class="avatar-circle dontcloseonclick" [ngClass]="{'active': showAccount}"> <div class="avatar-circle dontcloseonclick" [ngClass]="{'active': showAccount}">
<img class="avatar dontcloseonclick" *ngIf="componentCssClass == 'dark-theme'; else lighttheme" <i *ngIf="componentCssClass == 'dark-theme'; else lighttheme"
src="../assets/images/account-circle-outline.png" /> class="avatar dontcloseonclick las la-user-circle"></i>
<ng-template #lighttheme> <ng-template #lighttheme>
<img class="avatar dontcloseonclick" src="../assets/images/account-circle-outline-dark.png" /> <i class="avatar las la-user-circle"></i>
</ng-template> </ng-template>
</div> </div>
</div> </div>
@ -58,31 +59,31 @@
<div class="list"> <div class="list">
<a *ngIf="authService.authenticationChanged | async" class="nav-item" [routerLinkActive]="['active']" <a *ngIf="authService.authenticationChanged | async" class="nav-item" [routerLinkActive]="['active']"
[routerLinkActiveOptions]="{ exact: true }" [routerLink]="['/user/me']"> [routerLinkActiveOptions]="{ exact: true }" [routerLink]="['/user/me']">
<mat-icon class="icon" svgIcon="mdi_account_circle_outline"></mat-icon> <i class="icon las la-user-circle"></i>
<span class="label">{{ 'MENU.PERSONAL_INFO' | translate }}</span> <span class="label">{{ 'MENU.PERSONAL_INFO' | translate }}</span>
</a> </a>
<a *ngIf="showOrgSection && org?.id" class="nav-item" [routerLinkActive]="['active']" <a *ngIf="showOrgSection && org?.id" class="nav-item" [routerLinkActive]="['active']"
[routerLink]="[ '/orgs', org.id]"> [routerLink]="[ '/orgs', org.id]">
<mat-icon class="icon">business</mat-icon> <i class="icon las la-archway"></i>
<span class="label">{{org?.name ? org.name : 'MENU.ORGANIZATION' | translate}}</span> <span class="label">{{org?.name ? org.name : 'MENU.ORGANIZATION' | translate}}</span>
</a> </a>
<a *ngIf="showProjectSection" class="nav-item" [routerLinkActive]="['active']" <a *ngIf="showProjectSection" class="nav-item" [routerLinkActive]="['active']"
[routerLink]="[ '/projects']"> [routerLink]="[ '/projects']">
<mat-icon class="icon">folder_open</mat-icon> <i class="icon las la-layer-group"></i>
<span class="label">{{ 'MENU.PROJECT' | translate }}</span> <span class="label">{{ 'MENU.PROJECT' | translate }}</span>
</a> </a>
<a *ngIf="showUserSection" class="nav-item" [routerLinkActive]="['active']" [routerLink]="[ '/users']" <a *ngIf="showUserSection" class="nav-item" [routerLinkActive]="['active']" [routerLink]="[ '/users']"
[routerLinkActiveOptions]="{ exact: true }"> [routerLinkActiveOptions]="{ exact: true }">
<mat-icon class="icon">people_outline</mat-icon> <i class="icon las la-users"></i>
<span class="label">{{ 'MENU.USER' | translate }}</span> <span class="label">{{ 'MENU.USER' | translate }}</span>
</a> </a>
<span class="fill-space"></span> <span class="fill-space"></span>
<a class="nav-item" (click)="authService.signout()"> <a class="nav-item" (click)="authService.signout()">
<mat-icon class="icon" svgIcon="mdi_logout"></mat-icon> <i class="icon las la-sign-out-alt"></i>
<span class="label">{{ 'MENU.LOGOUT' | translate }}</span> <span class="label">{{ 'MENU.LOGOUT' | translate }}</span>
</a> </a>

View File

@ -54,33 +54,28 @@
.avatar-wrapper { .avatar-wrapper {
display: flex; display: flex;
align-items: center; align-items: center;
color: white;
.avatar-circle { .avatar-circle {
height: 35px; height: 30px;
width: 35px; width: 30px;
font-size: 30px;
background-color: transparent; background-color: transparent;
border-radius: 50%; border-radius: 50%;
animation: background-color .2s ease-in; animation: background-color .2s ease-in;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
margin-left: .5rem;
.avatar { .avatar {
display: block; display: block;
margin: auto auto; margin: auto auto;
height: 30px; height: 30px;
width: 30px; width: 30px;
line-height: 35px; line-height: 30px;
font-size: 30px; font-size: 30px;
border-radius: 50%; border-radius: 50%;
text-align: center; text-align: center;
fill: white;
* {
fill: white;
color: white;
}
} }
&:hover, &.active { &:hover, &.active {

View File

@ -170,11 +170,6 @@ export class AppComponent implements OnDestroy {
this.domSanitizer.bypassSecurityTrustResourceUrl('assets/mdi/account-cancel-outline.svg'), this.domSanitizer.bypassSecurityTrustResourceUrl('assets/mdi/account-cancel-outline.svg'),
); );
this.matIconRegistry.addSvgIcon(
'mdi_logout',
this.domSanitizer.bypassSecurityTrustResourceUrl('assets/mdi/logout.svg'),
);
this.matIconRegistry.addSvgIcon( this.matIconRegistry.addSvgIcon(
'mdi_light_on', 'mdi_light_on',
this.domSanitizer.bypassSecurityTrustResourceUrl('assets/mdi/lightbulb-on-outline.svg'), this.domSanitizer.bypassSecurityTrustResourceUrl('assets/mdi/lightbulb-on-outline.svg'),

View File

@ -19,7 +19,7 @@
margin: 0; margin: 0;
font-weight: 400; font-weight: 400;
font-family: 'Rubik'; font-family: 'Rubik';
font-size: 1.2rem; font-size: 18px;
// margin-top: .3rem; // margin-top: .3rem;
} }

View File

@ -10,4 +10,5 @@
<div class="sp-wrapper"> <div class="sp-wrapper">
<mat-spinner *ngIf="loading | async" diameter="25"></mat-spinner> <mat-spinner *ngIf="loading | async" diameter="25"></mat-spinner>
</div> </div>
<span class="err-container" *ngIf="errorMessage">{{errorMessage}}</span>
</div> </div>

View File

@ -2,7 +2,6 @@
display: block; display: block;
margin-bottom: 1rem; margin-bottom: 1rem;
font-weight: 400; font-weight: 400;
color: #81868a;
margin-top: 1rem; margin-top: 1rem;
} }
@ -37,4 +36,9 @@
display: flex; display: flex;
justify-content: center; justify-content: center;
} }
.err-container {
font-size: 14px;
color: rgb(201,51,71);
}
} }

View File

@ -1,7 +1,7 @@
import { Component, Input, OnInit } from '@angular/core'; import { Component, Input, OnInit } from '@angular/core';
import { Timestamp } from 'google-protobuf/google/protobuf/timestamp_pb'; import { Timestamp } from 'google-protobuf/google/protobuf/timestamp_pb';
import { BehaviorSubject, from, Observable } from 'rxjs'; import { BehaviorSubject, from, Observable, of } from 'rxjs';
import { scan, take, tap } from 'rxjs/operators'; import { catchError, scan, take, tap } from 'rxjs/operators';
import { Change, Changes } from 'src/app/proto/generated/management_pb'; import { Change, Changes } from 'src/app/proto/generated/management_pb';
import { MgmtUserService } from 'src/app/services/mgmt-user.service'; import { MgmtUserService } from 'src/app/services/mgmt-user.service';
@ -19,6 +19,7 @@ export enum ChangeType {
export class ChangesComponent implements OnInit { export class ChangesComponent implements OnInit {
@Input() public changeType: ChangeType = ChangeType.USER; @Input() public changeType: ChangeType = ChangeType.USER;
@Input() public id: string = ''; @Input() public id: string = '';
public errorMessage: string = '';
// Source data // Source data
private _done: BehaviorSubject<any> = new BehaviorSubject(false); private _done: BehaviorSubject<any> = new BehaviorSubject(false);
@ -50,7 +51,6 @@ export class ChangesComponent implements OnInit {
break; break;
case ChangeType.ORG: first = this.mgmtUserService.OrgChanges(this.id, 10, 0); case ChangeType.ORG: first = this.mgmtUserService.OrgChanges(this.id, 10, 0);
break; break;
} }
this.mapAndUpdate(first); this.mapAndUpdate(first);
@ -100,6 +100,7 @@ export class ChangesComponent implements OnInit {
// Map snapshot with doc ref (needed for cursor) // Map snapshot with doc ref (needed for cursor)
return from(col).pipe( return from(col).pipe(
tap((res: Changes) => { tap((res: Changes) => {
console.log('more cahnge');
let values = res.toObject().changesList; let values = res.toObject().changesList;
// If prepending, reverse the batch order // If prepending, reverse the batch order
values = false ? values.reverse() : values; values = false ? values.reverse() : values;
@ -114,7 +115,14 @@ export class ChangesComponent implements OnInit {
this._done.next(true); this._done.next(true);
} }
}), }),
take(1)).subscribe(); catchError(err => {
console.error(err);
this._loading.next(false);
this.errorMessage = err.message;
return of([]);
}),
take(1),
).subscribe();
} }
public dateFromTimestamp(date: Timestamp.AsObject): any { public dateFromTimestamp(date: Timestamp.AsObject): any {

View File

@ -31,6 +31,7 @@ export class ProjectRolesDataSource extends DataSource<ProjectRole.AsObject> {
catchError(() => of([])), catchError(() => of([])),
finalize(() => this.loadingSubject.next(false)), finalize(() => this.loadingSubject.next(false)),
).subscribe(roles => { ).subscribe(roles => {
console.log(roles);
this.rolesSubject.next(roles); this.rolesSubject.next(roles);
}); });
} }

View File

@ -44,9 +44,9 @@
</td> </td>
</ng-container> </ng-container>
<ng-container matColumnDef="name"> <ng-container matColumnDef="key">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.ROLE.NAME' | translate }} </th> <th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.ROLE.KEY' | translate }} </th>
<td mat-cell *matCellDef="let role"> {{role.name}} </td> <td mat-cell *matCellDef="let role"> {{role.key}} </td>
</ng-container> </ng-container>
<ng-container matColumnDef="displayname"> <ng-container matColumnDef="displayname">
@ -63,6 +63,15 @@
</td> </td>
</ng-container> </ng-container>
<ng-container matColumnDef="creationDate">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.ROLE.CREATIONDATE' | translate }} </th>
<td mat-cell *matCellDef="let role">
<span>{{dateFromTimestamp(role.creationDate) | date: 'dd. MMM, HH:mm' }}</span>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr> <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr> <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table> </table>

View File

@ -2,6 +2,7 @@ import { SelectionModel } from '@angular/cdk/collections';
import { AfterViewInit, Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'; import { AfterViewInit, Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core';
import { MatPaginator } from '@angular/material/paginator'; import { MatPaginator } from '@angular/material/paginator';
import { MatTable } from '@angular/material/table'; import { MatTable } from '@angular/material/table';
import { Timestamp } from 'google-protobuf/google/protobuf/timestamp_pb';
import { tap } from 'rxjs/operators'; import { tap } from 'rxjs/operators';
import { ProjectRole } from 'src/app/proto/generated/management_pb'; import { ProjectRole } from 'src/app/proto/generated/management_pb';
import { ProjectService } from 'src/app/services/project.service'; import { ProjectService } from 'src/app/services/project.service';
@ -26,7 +27,7 @@ export class ProjectRolesComponent implements AfterViewInit, OnInit {
@Output() public changedSelection: EventEmitter<Array<ProjectRole.AsObject>> = new EventEmitter(); @Output() public changedSelection: EventEmitter<Array<ProjectRole.AsObject>> = new EventEmitter();
/** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */ /** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */
public displayedColumns: string[] = ['select', 'name', 'displayname', 'group']; public displayedColumns: string[] = ['select', 'key', 'displayname', 'group', 'creationDate'];
constructor(private projectService: ProjectService, private toast: ToastService) { } constructor(private projectService: ProjectService, private toast: ToastService) { }
@ -107,4 +108,9 @@ export class ProjectRolesComponent implements AfterViewInit, OnInit {
this.toast.showError(data.message); this.toast.showError(data.message);
}); });
} }
public dateFromTimestamp(date: Timestamp.AsObject): any {
const ts: Date = new Date(date.seconds * 1000 + date.nanos / 1000);
return ts;
}
} }

View File

@ -12,7 +12,6 @@ import {
SearchMethod, SearchMethod,
} from 'src/app/proto/generated/management_pb'; } from 'src/app/proto/generated/management_pb';
import { ProjectService } from 'src/app/services/project.service'; import { ProjectService } from 'src/app/services/project.service';
import { ToastService } from 'src/app/services/toast.service';
@Component({ @Component({
selector: 'app-search-project-autocomplete', selector: 'app-search-project-autocomplete',
@ -33,7 +32,7 @@ export class SearchProjectAutocompleteComponent {
@ViewChild('auto') public matAutocomplete!: MatAutocomplete; @ViewChild('auto') public matAutocomplete!: MatAutocomplete;
@Input() public singleOutput: boolean = false; @Input() public singleOutput: boolean = false;
@Output() public selectionChanged: EventEmitter<Project.AsObject[] | Project.AsObject> = new EventEmitter(); @Output() public selectionChanged: EventEmitter<Project.AsObject[] | Project.AsObject> = new EventEmitter();
constructor(private projectService: ProjectService, private toast: ToastService) { constructor(private projectService: ProjectService) {
this.myControl.valueChanges this.myControl.valueChanges
.pipe( .pipe(
debounceTime(200), debounceTime(200),

View File

@ -107,6 +107,7 @@ export class AppCreateComponent implements OnInit, OnDestroy {
this.showSavedDialog(data.toObject()); this.showSavedDialog(data.toObject());
}) })
.catch(data => { .catch(data => {
console.error(data);
this.toast.showError(data.message); this.toast.showError(data.message);
}); });
} }

View File

@ -8,16 +8,18 @@
<p class="desc">{{ 'APP.PAGES.DESCRIPTION' | translate }}</p> <p class="desc">{{ 'APP.PAGES.DESCRIPTION' | translate }}</p>
</div> </div>
<span *ngIf="errorMessage" class="err-container">{{errorMessage}}</span>
<app-card title="{{ 'APP.PAGES.DETAIL.TITLE' | translate }}" *ngIf="app"> <app-card title="{{ 'APP.PAGES.DETAIL.TITLE' | translate }}" *ngIf="app">
<form [formGroup]="appNameForm" (ngSubmit)="saveOIDCApp()"> <form [formGroup]="appNameForm" (ngSubmit)="saveOIDCApp()">
<div class="content"> <div class="content">
<mat-button-toggle-group formControlName="state" class="toggle" (change)="changeState($event)"> <mat-button-toggle-group formControlName="state" class="toggle" (change)="changeState($event)">
<mat-button-toggle [value]="AppState.APPSTATE_INACTIVE" matTooltip="Deactivate Org"> <mat-button-toggle [value]="AppState.APPSTATE_INACTIVE" matTooltip="Deactivate Org">
<mat-icon svgIcon="mdi_light_off"></mat-icon> <i class="las la-toggle-off"></i>
{{'APP.PAGES.DETAIL.STATE.'+AppState.APPSTATE_INACTIVE | translate}} {{'APP.PAGES.DETAIL.STATE.'+AppState.APPSTATE_INACTIVE | translate}}
</mat-button-toggle> </mat-button-toggle>
<mat-button-toggle [value]="AppState.APPSTATE_ACTIVE" matTooltip="Activate Org"> <mat-button-toggle [value]="AppState.APPSTATE_ACTIVE" matTooltip="Activate Org">
<mat-icon svgIcon="mdi_light_on"></mat-icon> <i class="las la-toggle-on"></i>
{{'APP.PAGES.DETAIL.STATE.'+AppState.APPSTATE_ACTIVE | translate}} {{'APP.PAGES.DETAIL.STATE.'+AppState.APPSTATE_ACTIVE | translate}}
</mat-button-toggle> </mat-button-toggle>
</mat-button-toggle-group> </mat-button-toggle-group>

View File

@ -23,6 +23,11 @@
} }
} }
.err-container {
color: rgb(201,51,71);
font-size: 14px;
}
.card-actions { .card-actions {
button { button {
border-radius: .5rem; border-radius: .5rem;
@ -57,6 +62,10 @@
margin-bottom: 1rem; margin-bottom: 1rem;
margin-right: 1rem; margin-right: 1rem;
border-radius: .5rem; border-radius: .5rem;
i {
margin-right: 1rem;
}
} }
} }

View File

@ -33,6 +33,7 @@ enum RedirectType {
styleUrls: ['./app-detail.component.scss'], styleUrls: ['./app-detail.component.scss'],
}) })
export class AppDetailComponent implements OnInit, OnDestroy { export class AppDetailComponent implements OnInit, OnDestroy {
public errorMessage: string = '';
public selectable: boolean = false; public selectable: boolean = false;
public removable: boolean = true; public removable: boolean = true;
public addOnBlur: boolean = true; public addOnBlur: boolean = true;
@ -103,25 +104,31 @@ export class AppDetailComponent implements OnInit, OnDestroy {
private async getData({ projectid, id }: Params): Promise<void> { private async getData({ projectid, id }: Params): Promise<void> {
this.projectId = projectid; this.projectId = projectid;
this.app = (await this.projectService.GetApplicationById(projectid, id)).toObject(); this.projectService.GetApplicationById(projectid, id).then(app => {
this.appNameForm.patchValue(this.app); this.app = app.toObject();
if (this.app.state !== AppState.APPSTATE_ACTIVE) { this.appNameForm.patchValue(this.app);
this.appNameForm.controls['name'].disable(); if (this.app.state !== AppState.APPSTATE_ACTIVE) {
this.appForm.disable(); this.appNameForm.controls['name'].disable();
} else { this.appForm.disable();
this.appNameForm.controls['name'].enable(); } else {
this.appForm.enable(); this.appNameForm.controls['name'].enable();
this.clientId?.disable(); this.appForm.enable();
} this.clientId?.disable();
if (this.app.oidcConfig?.redirectUrisList) { }
this.redirectUrisList = this.app.oidcConfig.redirectUrisList; 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?.postLogoutRedirectUrisList) {
} this.postLogoutRedirectUrisList = this.app.oidcConfig.postLogoutRedirectUrisList;
if (this.app.oidcConfig) { }
this.appForm.patchValue(this.app.oidcConfig); if (this.app.oidcConfig) {
} this.appForm.patchValue(this.app.oidcConfig);
}
}).catch(error => {
console.error(error);
this.toast.showError(error.message);
this.errorMessage = error.message;
});
} }
public changeState(event: MatButtonToggleChange): void { public changeState(event: MatButtonToggleChange): void {

View File

@ -13,22 +13,20 @@
<div class="container"> <div class="container">
<div matTooltip="{{'ORG.PAGES.SELECTORGTOOLTIP' | translate}}" class="item card" <div matTooltip="{{'ORG.PAGES.SELECTORGTOOLTIP' | translate}}" class="item card"
*ngFor="let org of orgList; index as i" (click)="selectOrg(org, $event)" *ngFor="let org of orgList; index as i" (click)="selectOrg(org, $event)"
[ngClass]="{ selected: selection.isSelected(org) }"> [ngClass]="{ selected: selection.isSelected(org),active: activeOrg?.id === org?.id }">
<!-- <mat-icon matTooltip="select org" (click)="selection.toggle(org)" class="selection-icon"> <!-- <mat-icon matTooltip="select org" (click)="selection.toggle(org)" class="selection-icon">
check_circle</mat-icon> --> check_circle</mat-icon> -->
<div class="text-part"> <div class="text-part">
<span *ngIf="org?.changeDate" class="top">last modified on <!-- <span *ngIf="org?.changeDate" class="top">last modified on
{{ {{
dateFromTimestamp(org.changeDate) | date: 'EEE dd. MMM, HH:mm' dateFromTimestamp(org.changeDate) | date: 'EEE dd. MMM, HH:mm'
}}</span> }}</span> -->
<span class="description">{{org.id}}</span>
<span class="name" *ngIf="org.name">{{ org.name }}</span> <span class="name" *ngIf="org.name">{{ org.name }}</span>
<span class="name" *ngIf="!org.name">No Name</span> <span class="name" *ngIf="!org.name">No Name</span>
<span class="description">{{org.id}}</span>
<span class="fill-space"></span> <span class="fill-space"></span>
<div class="icons"> <div class="icons">
<div class="current" *ngIf="activeOrg?.id === org?.id">
<span>{{'ORG.PAGES.ACTIVE' | translate}}</span></div>
</div> </div>
</div> </div>
<button [matMenuTriggerFor]="editMenu" class="edit-button" mat-icon-button> <button [matMenuTriggerFor]="editMenu" class="edit-button" mat-icon-button>
@ -37,12 +35,12 @@
<mat-menu #editMenu="matMenu"> <mat-menu #editMenu="matMenu">
<ng-template matMenuContent> <ng-template matMenuContent>
<button (click)="selectOrg(org)" mat-menu-item> <button (click)="routeToOrg(org)" mat-menu-item>
{{'ACTIONS.VIEW' | translate}} {{'ACTIONS.VIEW' | translate}}
</button> </button>
<button (click)="selection.toggle(org)" mat-menu-item> <!-- <button (click)="selection.toggle(org)" mat-menu-item>
{{'ACTIONS.INFO' | translate}} {{'ACTIONS.INFO' | translate}}
</button> </button> -->
</ng-template> </ng-template>
</mat-menu> </mat-menu>
</div> </div>

View File

@ -52,6 +52,10 @@ h1 {
box-sizing: border-box; box-sizing: border-box;
} }
&.active {
border-color: #db4c69;
}
.selection-icon { .selection-icon {
opacity: 0; opacity: 0;
position: absolute; position: absolute;
@ -88,6 +92,7 @@ h1 {
.description { .description {
font-size: 0.8rem; font-size: 0.8rem;
margin-top: .5rem;
} }
.created { .created {
@ -120,6 +125,7 @@ h1 {
.current { .current {
height: 10px; height: 10px;
font-size: 14px;
width: 10px; width: 10px;
border-radius: 50%; border-radius: 50%;
background-color: rgb(144,212,210); background-color: rgb(144,212,210);

View File

@ -42,10 +42,14 @@ export class OrgGridComponent {
public selectOrg(item: Org.AsObject, event?: any): void { public selectOrg(item: Org.AsObject, event?: any): void {
if (event && !event.target.classList.contains('mat-icon')) { if (event && !event.target.classList.contains('mat-icon')) {
this.authService.setActiveOrg(item); this.authService.setActiveOrg(item);
this.router.navigate(['/orgs', item.id]); this.routeToOrg(item);
} }
} }
public routeToOrg(item: Org.AsObject): void {
this.router.navigate(['/orgs', item.id]);
}
public dateFromTimestamp(date: Timestamp.AsObject): any { public dateFromTimestamp(date: Timestamp.AsObject): any {
const ts: Date = new Date(date.seconds * 1000 + date.nanos / 1000); const ts: Date = new Date(date.seconds * 1000 + date.nanos / 1000);
return ts; return ts;

View File

@ -31,6 +31,7 @@ export class OrgMembersDataSource extends DataSource<OrgMember.AsObject> {
catchError(() => of([])), catchError(() => of([])),
finalize(() => this.loadingSubject.next(false)), finalize(() => this.loadingSubject.next(false)),
).subscribe(members => { ).subscribe(members => {
console.log(members);
this.membersSubject.next(members); this.membersSubject.next(members);
}); });
} }

View File

@ -3,7 +3,7 @@
<p class="top-desc">{{'ORG.POLICY.DESCRIPTION' | translate}}</p> <p class="top-desc">{{'ORG.POLICY.DESCRIPTION' | translate}}</p>
<div class="row-lyt"> <div class="row-lyt">
<div class="p-item card"> <!-- <div class="p-item card">
<div class="avatar"> <div class="avatar">
<mat-icon svgIcon="mdi_lock_reset"></mat-icon> <mat-icon svgIcon="mdi_lock_reset"></mat-icon>
</div> </div>
@ -38,7 +38,7 @@
<button [disabled]="!agePolicy" [routerLink]="[ 'policy', PolicyComponentType.AGE ]" <button [disabled]="!agePolicy" [routerLink]="[ 'policy', PolicyComponentType.AGE ]"
mat-raised-button>{{'ORG.POLICY.BTN_EDIT' | translate}}</button> mat-raised-button>{{'ORG.POLICY.BTN_EDIT' | translate}}</button>
</div> </div>
</div> </div> -->
<div class="p-item card"> <div class="p-item card">
<div class="avatar"> <div class="avatar">
<mat-icon svgIcon="mdi_textbox_password"></mat-icon> <mat-icon svgIcon="mdi_textbox_password"></mat-icon>
@ -70,7 +70,7 @@
mat-raised-button>{{'ORG.POLICY.BTN_EDIT' | translate}}</button> mat-raised-button>{{'ORG.POLICY.BTN_EDIT' | translate}}</button>
</div> </div>
</div> </div>
<div class="p-item card"> <!-- <div class="p-item card">
<div class="avatar"> <div class="avatar">
<mat-icon svgIcon="mdi_lock_question"></mat-icon> <mat-icon svgIcon="mdi_lock_question"></mat-icon>
</div> </div>
@ -98,5 +98,5 @@
<button [disabled]="!lockoutPolicy" [routerLink]="[ 'policy', PolicyComponentType.LOCKOUT ]" <button [disabled]="!lockoutPolicy" [routerLink]="[ 'policy', PolicyComponentType.LOCKOUT ]"
mat-raised-button>{{'ORG.POLICY.BTN_EDIT' | translate}}</button> mat-raised-button>{{'ORG.POLICY.BTN_EDIT' | translate}}</button>
</div> </div>
</div> </div> -->
</div> </div>

View File

@ -32,8 +32,8 @@ export class PolicyGridComponent implements OnInit {
} }
private getData(): void { private getData(): void {
this.orgService.GetPasswordLockoutPolicy().then(data => this.lockoutPolicy = data.toObject()).catch(error => { }); // this.orgService.GetPasswordLockoutPolicy().then(data => this.lockoutPolicy = data.toObject()).catch(error => { });
this.orgService.GetPasswordAgePolicy().then(data => this.agePolicy = data.toObject()).catch(error => { }); // this.orgService.GetPasswordAgePolicy().then(data => this.agePolicy = data.toObject()).catch(error => { });
this.orgService.GetPasswordComplexityPolicy().then(data => this.complexityPolicy = data.toObject()) this.orgService.GetPasswordComplexityPolicy().then(data => this.complexityPolicy = data.toObject())
.catch(error => { }); .catch(error => { });
} }

View File

@ -15,6 +15,11 @@
<form @list (ngSubmit)="addRole()"> <form @list (ngSubmit)="addRole()">
<div @animate *ngFor="let formGroup of formArray.controls; index as i" class="content"> <div @animate *ngFor="let formGroup of formArray.controls; index as i" class="content">
<ng-container [formGroup]="formGroup"> <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="name?.errors?.required">{{'ERRORS.REQUIRED' | translate}}</mat-error> -->
</mat-form-field>
<mat-form-field appearance="outline" class="formfield"> <mat-form-field appearance="outline" class="formfield">
<mat-label>{{ 'PROJECT.ROLE.NAME' | translate }}</mat-label> <mat-label>{{ 'PROJECT.ROLE.NAME' | translate }}</mat-label>
<input matInput formControlName="name" /> <input matInput formControlName="name" />

View File

@ -50,6 +50,7 @@ export class ProjectRoleCreateComponent implements OnInit, OnDestroy {
private fb: FormBuilder, private fb: FormBuilder,
) { ) {
this.formGroup = new FormGroup({ this.formGroup = new FormGroup({
key: new FormControl(''),
name: new FormControl(''), name: new FormControl(''),
displayName: new FormControl(''), displayName: new FormControl(''),
group: new FormControl('', [Validators.required]), group: new FormControl('', [Validators.required]),

View File

@ -2,7 +2,7 @@
<h3>{{'APP.LIST' | translate}}</h3> <h3>{{'APP.LIST' | translate}}</h3>
<span class="fill-space"></span> <span class="fill-space"></span>
<button mat-icon-button (click)="closeView()"> <button mat-icon-button (click)="closeView()">
<mat-icon matTooltip="show list view">list</mat-icon> <i class="show list view las la-th-list"></i>
</button> </button>
</div> </div>
<div class="app-container"> <div class="app-container">

View File

@ -18,6 +18,7 @@
padding-bottom: 2rem; padding-bottom: 2rem;
.app-wrap { .app-wrap {
outline: none;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;

View File

@ -73,4 +73,8 @@
.pointer { .pointer {
outline: none; outline: none;
cursor: pointer; cursor: pointer;
} }
tr {
outline: none;
}

View File

@ -5,15 +5,15 @@
<div class="img-list"> <div class="img-list">
<ng-container *ngIf="totalResult < 10; else compact"> <ng-container *ngIf="totalResult < 10; else compact">
<ng-container *ngFor="let member of membersSubject | async"> <ng-container *ngFor="let member of membersSubject | async">
<img (click)="showDetail()" *ngIf="member.imageURL; else render" class="avatar-img" <!-- <img (click)="showDetail()" *ngIf="member.imageURL; else render" class="avatar-img"
[src]="member.imageURL" matTooltip="{{ member.email }} | {{member.rolesList?.join(' ')}}" [src]="member.imageURL" matTooltip="{{ member.email }} | {{member.rolesList?.join(' ')}}"
alt="editor avatar" /> alt="editor avatar" />
<ng-template #render> <ng-template #render> -->
<div (click)="showDetail()" class="avatar-circle" <div (click)="showDetail()" class="avatar-circle"
matTooltip="{{ member.email }} | {{member.rolesList?.join(' ')}}"> matTooltip="{{ member.email }} | {{member.rolesList?.join(' ')}}">
<mat-icon>face</mat-icon> <mat-icon>face</mat-icon>
</div> </div>
</ng-template> <!-- </ng-template> -->
</ng-container> </ng-container>
</ng-container> </ng-container>
<ng-template #compact> <ng-template #compact>
@ -22,7 +22,7 @@
</div> </div>
</ng-template> </ng-template>
<button class="add-img" (click)="openAddMember()" <button class="add-img" (click)="openAddMember()"
[disabled]="project?.state !== ProjectState.ACTIVE_PROJECT" mat-icon-button [disabled]="project?.state !== ProjectState.PROJECTSTATE_ACTIVE" mat-icon-button
aria-label="Edit contributors"> aria-label="Edit contributors">
<mat-icon>add</mat-icon> <mat-icon>add</mat-icon>
</button> </button>

View File

@ -5,7 +5,6 @@
display: block; display: block;
margin-bottom: 1rem; margin-bottom: 1rem;
font-weight: 400; font-weight: 400;
color: #81868a;
} }
.sub-header { .sub-header {

View File

@ -6,8 +6,8 @@ import { catchError, finalize, map } from 'rxjs/operators';
import { User } from 'src/app/proto/generated/auth_pb'; import { User } from 'src/app/proto/generated/auth_pb';
import { import {
GrantedProject, GrantedProject,
ProjectMember,
ProjectMemberSearchResponse, ProjectMemberSearchResponse,
ProjectMemberView,
ProjectState, ProjectState,
ProjectType, ProjectType,
} from 'src/app/proto/generated/management_pb'; } from 'src/app/proto/generated/management_pb';
@ -26,10 +26,13 @@ import {
}) })
export class ProjectContributorsComponent implements OnInit { export class ProjectContributorsComponent implements OnInit {
@Input() public project!: GrantedProject.AsObject; @Input() public project!: GrantedProject.AsObject;
@Input() public projectType!: ProjectType;
@Input() public disabled: boolean = false; @Input() public disabled: boolean = false;
public totalResult: number = 0; public totalResult: number = 0;
public membersSubject: BehaviorSubject<ProjectMember.AsObject[]> = new BehaviorSubject<ProjectMember.AsObject[]>([]); public membersSubject: BehaviorSubject<ProjectMemberView.AsObject[]>
= new BehaviorSubject<ProjectMemberView.AsObject[]>([]);
public ProjectState: any = ProjectState; public ProjectState: any = ProjectState;
private loadingSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false); private loadingSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
@ -39,10 +42,11 @@ export class ProjectContributorsComponent implements OnInit {
private router: Router) { } private router: Router) { }
public ngOnInit(): void { public ngOnInit(): void {
console.log(this.project);
const promise: Promise<ProjectMemberSearchResponse> | undefined = const promise: Promise<ProjectMemberSearchResponse> | undefined =
this.project.type === ProjectType.PROJECTTYPE_OWNED ? this.projectType === ProjectType.PROJECTTYPE_OWNED ?
this.projectService.SearchProjectMembers(this.project.id, 100, 0) : this.projectService.SearchProjectMembers(this.project.id, 100, 0) :
this.project.type === ProjectType.PROJECTTYPE_GRANTED ? this.projectType === ProjectType.PROJECTTYPE_GRANTED ?
this.projectService.SearchProjectGrantMembers(this.project.id, this.project.grantId, 100, 0) : undefined; this.projectService.SearchProjectGrantMembers(this.project.id, this.project.grantId, 100, 0) : undefined;
if (promise) { if (promise) {
from(promise).pipe( from(promise).pipe(

View File

@ -43,7 +43,7 @@
<app-card *ngIf="!grid" title="{{ 'PROJECT.APP.TITLE' | translate }}"> <app-card *ngIf="!grid" title="{{ 'PROJECT.APP.TITLE' | translate }}">
<card-actions class="card-actions"> <card-actions class="card-actions">
<button mat-icon-button (click)="grid = true"> <button mat-icon-button (click)="grid = true">
<mat-icon matTooltip="show grid view">grid_on</mat-icon> <i matTooltip="show grid view" class="las la-th-large"></i>
</button> </button>
</card-actions> </card-actions>
<app-project-applications [disabled]="project?.state !== ProjectState.PROJECTSTATE_ACTIVE" <app-project-applications [disabled]="project?.state !== ProjectState.PROJECTSTATE_ACTIVE"
@ -81,7 +81,6 @@
</ng-template> </ng-template>
</div> </div>
<metainfo class="side"> <metainfo class="side">
<div class="details"> <div class="details">
<div class="row"> <div class="row">
<span class="first">{{'PROJECT.TYPE.TITLE' | translate}}:</span> <span class="first">{{'PROJECT.TYPE.TITLE' | translate}}:</span>
@ -98,7 +97,8 @@
<mat-tab-group mat-stretch-tabs class="tab-group" disablePagination="true"> <mat-tab-group mat-stretch-tabs class="tab-group" disablePagination="true">
<mat-tab label="Details"> <mat-tab label="Details">
<app-project-contributors *ngIf="project" <app-project-contributors *ngIf="project"
[disabled]="project?.state !== ProjectState.PROJECTSTATE_ACTIVE" [project]="project"> [disabled]="project?.state !== ProjectState.PROJECTSTATE_ACTIVE" [projectType]="projectType"
[project]="project">
</app-project-contributors> </app-project-contributors>
</mat-tab> </mat-tab>
<mat-tab label="{{ 'CHANGES.PROJECT.TITLE' | translate }}" class="flex-col"> <mat-tab label="{{ 'CHANGES.PROJECT.TITLE' | translate }}" class="flex-col">

View File

@ -82,6 +82,7 @@ export class ProjectDetailComponent implements OnInit, OnDestroy {
this.grantId = grantId; this.grantId = grantId;
if (grantId) { if (grantId) {
this.projectType = ProjectType.PROJECTTYPE_GRANTED;
// this.projectService.GetGrantedProjectGrantByID(id, this.grantId).then(proj => { // this.projectService.GetGrantedProjectGrantByID(id, this.grantId).then(proj => {
// this.projectGrant = proj.toObject(); // this.projectGrant = proj.toObject();
// this.isZitadel$ = from(this.projectService.SearchApplications(this.project.id, 100, 0).then(appsResp => { // this.isZitadel$ = from(this.projectService.SearchApplications(this.project.id, 100, 0).then(appsResp => {
@ -93,6 +94,7 @@ export class ProjectDetailComponent implements OnInit, OnDestroy {
// this.toast.showError(error.message); // this.toast.showError(error.message);
// }); // });
} else { } else {
this.projectType = ProjectType.PROJECTTYPE_OWNED;
this.projectService.GetProjectById(id).then(proj => { this.projectService.GetProjectById(id).then(proj => {
this.project = proj.toObject(); this.project = proj.toObject();
// if (this.project.type !== ProjectType.PROJECTTYPE_SELF || // if (this.project.type !== ProjectType.PROJECTTYPE_SELF ||
@ -105,6 +107,7 @@ export class ProjectDetailComponent implements OnInit, OnDestroy {
.filter(app => app.oidcConfig?.clientId === this.grpcService.clientid).length > 0; .filter(app => app.oidcConfig?.clientId === this.grpcService.clientid).length > 0;
return ret; return ret;
})); // TODO: replace with prettier thing })); // TODO: replace with prettier thing
this.isZitadel$.subscribe(isZita => console.log(`zitade: ${isZita}`));
}).catch(error => { }).catch(error => {
this.toast.showError(error.message); this.toast.showError(error.message);
}); });

View File

@ -7,7 +7,7 @@
</app-search-user-autocomplete> </app-search-user-autocomplete>
<mat-form-field class="full-width"> <mat-form-field class="full-width">
<mat-label>{{ 'APP.OIDC.RESPONSE' | translate }}</mat-label> <mat-label>{{ 'PROJECT.MEMBER.ROLES' | translate }}</mat-label>
<mat-select [(ngModel)]="roleKeyList" multiple> <mat-select [(ngModel)]="roleKeyList" multiple>
<mat-option *ngFor="let key of data.roleKeysList" [value]="key"> <mat-option *ngFor="let key of data.roleKeysList" [value]="key">
{{ key }} {{ key }}

View File

@ -87,13 +87,15 @@
<span class="mem-title">Members</span> <span class="mem-title">Members</span>
<ng-template appHasRole <ng-template appHasRole
[appHasRole]="['project.grant.member.write:' + projectId, 'project.grant.member.write']"> [appHasRole]="['project.grant.member.write:' + projectId, 'project.grant.member.write']">
<button [disabled]="disabled" mat-icon-button <button [disabled]="disabled || element?.roleKeysList?.length === 0"
matTooltip="disabled or no roles defined" mat-icon-button
(click)="addProjectGrantMember(element)"> (click)="addProjectGrantMember(element)">
<mat-icon>add</mat-icon> <mat-icon>add</mat-icon>
</button> </button>
</ng-template> </ng-template>
</div> </div>
<div class="mem-description"> <div class="mem-description"
*ngIf="selectedGrantMembers && selectedGrantMembers.length > 0">
<span *ngFor="let mem of selectedGrantMembers">{{mem.firstName}} {{mem.lastName}} <span *ngFor="let mem of selectedGrantMembers">{{mem.firstName}} {{mem.lastName}}
{{mem.email}} | {{mem.email}} |
<span *ngFor="let role of mem.rolesList">{{role}} </span></span> <span *ngFor="let role of mem.rolesList">{{role}} </span></span>

View File

@ -6,7 +6,7 @@ import { MatPaginator } from '@angular/material/paginator';
import { MatTable } from '@angular/material/table'; import { MatTable } from '@angular/material/table';
import { Timestamp } from 'google-protobuf/google/protobuf/timestamp_pb'; import { Timestamp } from 'google-protobuf/google/protobuf/timestamp_pb';
import { tap } from 'rxjs/operators'; import { tap } from 'rxjs/operators';
import { ProjectGrant, ProjectGrantMember } from 'src/app/proto/generated/management_pb'; import { ProjectGrant, ProjectMemberView } from 'src/app/proto/generated/management_pb';
import { ProjectService } from 'src/app/services/project.service'; import { ProjectService } from 'src/app/services/project.service';
import { ToastService } from 'src/app/services/toast.service'; import { ToastService } from 'src/app/services/toast.service';
@ -36,7 +36,7 @@ export class ProjectGrantsComponent implements OnInit, AfterViewInit {
public dataSource!: ProjectGrantsDataSource; public dataSource!: ProjectGrantsDataSource;
public selection: SelectionModel<ProjectGrant.AsObject> = new SelectionModel<ProjectGrant.AsObject>(true, []); public selection: SelectionModel<ProjectGrant.AsObject> = new SelectionModel<ProjectGrant.AsObject>(true, []);
public expandedElement: ProjectGrant.AsObject | null = null; public expandedElement: ProjectGrant.AsObject | null = null;
public selectedGrantMembers: ProjectGrantMember.AsObject[] = []; public selectedGrantMembers: ProjectMemberView.AsObject[] = [];
/** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */ /** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */
public displayedColumns: string[] = ['select', 'grantedOrgName', 'grantedOrgDomain', 'creationDate', 'changeDate', 'roleNamesList']; public displayedColumns: string[] = ['select', 'grantedOrgName', 'grantedOrgDomain', 'creationDate', 'changeDate', 'roleNamesList'];
@ -97,6 +97,13 @@ export class ProjectGrantsComponent implements OnInit, AfterViewInit {
width: '400px', width: '400px',
}); });
console.log({
orgId: grant.grantedOrgId,
grantId: grant.id,
projectId: grant.projectId,
roleKeysList: grant.roleKeysList,
});
dialogRef.afterClosed().subscribe((dataToAdd: ProjectGrantMembersCreateDialogExportType) => { dialogRef.afterClosed().subscribe((dataToAdd: ProjectGrantMembersCreateDialogExportType) => {
if (dataToAdd) { if (dataToAdd) {
dataToAdd.userIds.forEach(userid => { dataToAdd.userIds.forEach(userid => {

View File

@ -3,23 +3,23 @@
<ng-template appHasRole [appHasRole]="['project.write']"> <ng-template appHasRole [appHasRole]="['project.write']">
<button (click)="deactivateProjects(selection.selected)" @animate <button (click)="deactivateProjects(selection.selected)" @animate
matTooltip="{{'PROJECT.TABLE.DEACTIVATE' | translate}}" class="left-button" mat-icon-button> matTooltip="{{'PROJECT.TABLE.DEACTIVATE' | translate}}" class="left-button" mat-icon-button>
<mat-icon svgIcon="mdi_light_off"></mat-icon> <i class="las la-toggle-off"></i>
</button> </button>
<button @animate (click)="reactivateProjects(selection.selected)" class="left-button" <button @animate (click)="reactivateProjects(selection.selected)" class="left-button"
matTooltip="{{'PROJECT.TABLE.ACTIVATE' | translate}}" mat-icon-button> matTooltip="{{'PROJECT.TABLE.ACTIVATE' | translate}}" mat-icon-button>
<mat-icon svgIcon="mdi_light_on"></mat-icon> <i class="las la-toggle-on"></i>
</button> </button>
</ng-template> </ng-template>
</div> </div>
<button [disabled]="selection.selected.length > 0" (click)="changedView.emit(true)" mat-icon-button> <button [disabled]="selection.selected.length > 0" (click)="changedView.emit(true)" mat-icon-button>
<mat-icon matTooltip="show list view">list</mat-icon> <i class="show list view las la-th-list"></i>
</button> </button>
</div> </div>
<div class="container"> <div class="container">
<mat-progress-bar *ngIf="loading" class="spinner" color="accent" mode="indeterminate"></mat-progress-bar> <mat-progress-bar *ngIf="loading" class="spinner" color="accent" mode="indeterminate"></mat-progress-bar>
<div class="item card" *ngFor="let item of items; index as i" (click)="selectItem(item, $event)" <div class="item card" *ngFor="let item of items; index as i" (click)="selectItem(item, $event)"
[ngClass]="{ selected: selection.isSelected(item), inactive: item.state !== ProjectState.ACTIVE_PROJECT}"> [ngClass]="{ selected: selection.isSelected(item), inactive: item.state !== ProjectState.PROJECTSTATE_ACTIVE}">
<mat-icon matTooltip="select item" (click)="selection.toggle(item)" class="selection-icon"> <mat-icon matTooltip="select item" (click)="selection.toggle(item)" class="selection-icon">
check_circle</mat-icon> check_circle</mat-icon>
<div class="text-part"> <div class="text-part">
@ -29,8 +29,8 @@
}}</span> }}</span>
<span class="name" *ngIf="item.name">{{ item.name }}</span> <span class="name" *ngIf="item.name">{{ item.name }}</span>
<span class="description" *ngIf="item.state">{{'PROJECT.STATE.'+item.state | translate}}</span> <span class="description" *ngIf="item.state">{{'PROJECT.STATE.'+item.state | translate}}</span>
<!-- <span class="description" *ngIf="item.type">{{'PROJECT.TYPE.TITLE' | translate}}: <span class="description" *ngIf="item.type !== undefined">{{'PROJECT.TYPE.TITLE' | translate}}:
{{'PROJECT.TYPE.'+item.type | translate}}</span> --> {{'PROJECT.TYPE.'+item.type | translate}}</span>
<span *ngIf="item.changeDate" class="created">created on <span *ngIf="item.changeDate" class="created">created on
{{ {{
dateFromTimestamp(item.creationDate) | date: 'EEE dd. MMM, HH:mm' dateFromTimestamp(item.creationDate) | date: 'EEE dd. MMM, HH:mm'
@ -52,8 +52,6 @@
<button (click)="selection.toggle(item)" mat-menu-item> <button (click)="selection.toggle(item)" mat-menu-item>
{{'ACTIONS.INFO' | translate}} {{'ACTIONS.INFO' | translate}}
</button> </button>
<button (click)="deleteProject(item)" mat-menu-item> {{'ACTIONS.DELETE' | translate}}</button>
</ng-template> </ng-template>
</mat-menu> </mat-menu>
</div> </div>

View File

@ -53,6 +53,10 @@
top: -12px; top: -12px;
left: -12px; left: -12px;
user-select: none; user-select: none;
&:hover {
color: white;
}
} }
img { img {
@ -121,6 +125,10 @@
color: #81868a; color: #81868a;
} }
} }
span {
margin: 2px 0;
}
} }
.edit-button { .edit-button {

View File

@ -32,12 +32,12 @@ import { ToastService } from 'src/app/services/toast.service';
], ],
}) })
export class ProjectGridComponent { export class ProjectGridComponent {
@Input() items: Array<Project.AsObject> = []; @Input() items: Array<GrantedProject.AsObject> = [];
@Output() newClicked: EventEmitter<boolean> = new EventEmitter(); @Output() newClicked: EventEmitter<boolean> = new EventEmitter();
@Output() changedView: EventEmitter<boolean> = new EventEmitter(); @Output() changedView: EventEmitter<boolean> = new EventEmitter();
@Input() loading: boolean = false; @Input() loading: boolean = false;
public selection: SelectionModel<Project.AsObject> = new SelectionModel<Project.AsObject>(true, []); public selection: SelectionModel<GrantedProject.AsObject> = new SelectionModel<GrantedProject.AsObject>(true, []);
public selectedIndex: number = -1; public selectedIndex: number = -1;
public showNewProject: boolean = false; public showNewProject: boolean = false;
@ -48,41 +48,25 @@ export class ProjectGridComponent {
public selectItem(item: GrantedProject.AsObject, event?: any): void { public selectItem(item: GrantedProject.AsObject, event?: any): void {
if (event && !event.target.classList.contains('mat-icon')) { if (event && !event.target.classList.contains('mat-icon')) {
if (item.grantId) { if (item.grantId) {
this.router.navigate(['/project-grant', `${item.id}:${item.grantId}`]); this.router.navigate([item.id, '/grant', `${item.grantId}`]);
} else { } else {
this.router.navigate(['/projects', item.id]); this.router.navigate(['/projects', item.id]);
} }
} else if (!event) { } else if (!event) {
this.router.navigate(['/projects', item.id]); if (item.grantId) {
this.router.navigate([item.id, '/grant', `${item.grantId}`]);
} else {
this.router.navigate(['/projects', item.id]);
}
} }
} }
public addItem(): void { public addItem(): void {
this.newClicked.emit(true); this.newClicked.emit(true);
} }
public deleteProjects(selected: Project.AsObject[]): void {
// TODO: implement service
// Promise.all([selected.map(proj => {
// return this.projectService.DeleteProject(proj.id);
// })]).then(() => {
// this.toast.showInfo('Successful deleted all projects');
// }).catch(error => {
// this.toast.showError(error.message);
// });
}
public deleteProject(proj: Project.AsObject): void {
// TODO: implement service
// this.projectService.DeleteProject(proj.id).then(() => {
// this.toast.showInfo('Successful deleted Project');
// }).catch(error => {
// this.toast.showError(error.message);
// });
}
public dateFromTimestamp(date: Timestamp.AsObject): any { public dateFromTimestamp(date: Timestamp.AsObject): any {
const ts: Date = new Date(date.seconds * 1000 + date.nanos / 1000); const ts: Date = new Date(date.seconds * 1000 + date.nanos / 1000);
return ts; return ts;

View File

@ -8,7 +8,7 @@
<div *ngIf="!grid" class="view-toggle"> <div *ngIf="!grid" class="view-toggle">
<button (click)="grid = true" mat-icon-button> <button (click)="grid = true" mat-icon-button>
<mat-icon matTooltip="show grid view">grid_on</mat-icon> <i matTooltip="show grid view" class="las la-th-large"></i>
</button> </button>
</div> </div>
<div *ngIf="!grid && projectList"> <div *ngIf="!grid && projectList">
@ -83,7 +83,7 @@
<ng-container matColumnDef="type"> <ng-container matColumnDef="type">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.TABLE.TYPE' | translate }} </th> <th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.TABLE.TYPE' | translate }} </th>
<td mat-cell *matCellDef="let project"> <td mat-cell *matCellDef="let project">
<span *ngIf="project.type">{{'PROJECT.TYPE.'+project.type | translate}}</span> <span *ngIf="project.type !== undefined">{{'PROJECT.TYPE.'+project.type | translate}}</span>
</td> </td>
</ng-container> </ng-container>

View File

@ -100,4 +100,8 @@ h1 {
max-width: 50px; max-width: 50px;
} }
} }
}
tr {
outline: none;
} }

View File

@ -1,6 +1,6 @@
import { animate, animateChild, query, stagger, style, transition, trigger } from '@angular/animations'; import { animate, animateChild, query, stagger, style, transition, trigger } from '@angular/animations';
import { SelectionModel } from '@angular/cdk/collections'; import { SelectionModel } from '@angular/cdk/collections';
import { Component, OnInit } from '@angular/core'; import { Component, OnDestroy, OnInit } from '@angular/core';
import { PageEvent } from '@angular/material/paginator'; import { PageEvent } from '@angular/material/paginator';
import { MatTableDataSource } from '@angular/material/table'; import { MatTableDataSource } from '@angular/material/table';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
@ -35,7 +35,7 @@ import { ToastService } from 'src/app/services/toast.service';
]), ]),
], ],
}) })
export class ProjectListComponent implements OnInit { export class ProjectListComponent implements OnInit, OnDestroy {
public totalResult: number = 0; public totalResult: number = 0;
public dataSource: MatTableDataSource<GrantedProject.AsObject> = new MatTableDataSource<GrantedProject.AsObject>(); public dataSource: MatTableDataSource<GrantedProject.AsObject> = new MatTableDataSource<GrantedProject.AsObject>();
public projectList: GrantedProject.AsObject[] = []; public projectList: GrantedProject.AsObject[] = [];
@ -58,6 +58,10 @@ export class ProjectListComponent implements OnInit {
this.subscription = this.route.params.subscribe(() => this.getData(10, 0)); this.subscription = this.route.params.subscribe(() => this.getData(10, 0));
} }
public ngOnDestroy(): void {
this.subscription?.unsubscribe();
}
public isAllSelected(): boolean { public isAllSelected(): boolean {
const numSelected = this.selection.selected.length; const numSelected = this.selection.selected.length;
const numRows = this.dataSource.data.length; const numRows = this.dataSource.data.length;

View File

@ -1,6 +1,7 @@
import { DataSource } from '@angular/cdk/collections'; import { DataSource } from '@angular/cdk/collections';
import { BehaviorSubject, Observable } from 'rxjs'; import { BehaviorSubject, from, Observable, of } from 'rxjs';
import { Project, ProjectMember } from 'src/app/proto/generated/management_pb'; import { catchError, finalize, map } from 'rxjs/operators';
import { Project, ProjectMember, ProjectMemberSearchResponse, ProjectType } from 'src/app/proto/generated/management_pb';
import { ProjectService } from 'src/app/services/project.service'; import { ProjectService } from 'src/app/services/project.service';
/** /**
@ -18,29 +19,32 @@ export class ProjectMembersDataSource extends DataSource<ProjectMember.AsObject>
super(); super();
} }
public loadMembers(project: Project.AsObject, pageIndex: number, pageSize: number, sortDirection?: string): void { public loadMembers(project: Project.AsObject,
projectType: ProjectType,
pageIndex: number, pageSize: number, grantId?: string, sortDirection?: string): void {
const offset = pageIndex * pageSize; const offset = pageIndex * pageSize;
this.loadingSubject.next(true); this.loadingSubject.next(true);
// TODO // TODO
// const promise: Promise<ProjectMemberSearchResponse> | undefined = const promise: Promise<ProjectMemberSearchResponse> | undefined =
// project.type === ProjectType.PROJECTTYPE_OWNED ? projectType === ProjectType.PROJECTTYPE_OWNED ?
// this.projectService.SearchProjectMembers(project.id, pageSize, offset) : this.projectService.SearchProjectMembers(project.id, pageSize, offset) :
// project.type === ProjectType.PROJECTTYPE_GRANTED ? projectType === ProjectType.PROJECTTYPE_GRANTED && grantId ?
// this.projectService.SearchProjectGrantMembers(project.id, this.projectService.SearchProjectGrantMembers(project.id,
// project.grantId, pageSize, offset) : undefined; grantId, pageSize, offset) : undefined;
// if (promise) { if (promise) {
// from(promise).pipe( from(promise).pipe(
// map(resp => { map(resp => {
// this.totalResult = resp.toObject().totalResult; this.totalResult = resp.toObject().totalResult;
// return resp.toObject().resultList; console.log(this.totalResult);
// }), return resp.toObject().resultList;
// catchError(() => of([])), }),
// finalize(() => this.loadingSubject.next(false)), catchError(() => of([])),
// ).subscribe(members => { finalize(() => this.loadingSubject.next(false)),
// this.membersSubject.next(members); ).subscribe(members => {
// }); this.membersSubject.next(members);
// } });
}
} }

View File

@ -1,11 +1,10 @@
import { SelectionModel } from '@angular/cdk/collections'; import { SelectionModel } from '@angular/cdk/collections';
import { AfterViewInit, Component, ViewChild } from '@angular/core'; import { AfterViewInit, Component, ViewChild } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { MatPaginator } from '@angular/material/paginator'; import { MatPaginator } from '@angular/material/paginator';
import { MatTable } from '@angular/material/table'; import { MatTable } from '@angular/material/table';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { tap } from 'rxjs/operators'; import { tap } from 'rxjs/operators';
import { Project, ProjectMember } from 'src/app/proto/generated/management_pb'; import { Project, ProjectMember, ProjectType } from 'src/app/proto/generated/management_pb';
import { ProjectService } from 'src/app/services/project.service'; import { ProjectService } from 'src/app/services/project.service';
import { ToastService } from 'src/app/services/toast.service'; import { ToastService } from 'src/app/services/toast.service';
@ -18,6 +17,7 @@ import { ProjectMembersDataSource } from './project-members-datasource';
}) })
export class ProjectMembersComponent implements AfterViewInit { export class ProjectMembersComponent implements AfterViewInit {
public project!: Project.AsObject; public project!: Project.AsObject;
public projectType: ProjectType = ProjectType.PROJECTTYPE_OWNED;
public disabled: boolean = false; public disabled: boolean = false;
@ViewChild(MatPaginator) public paginator!: MatPaginator; @ViewChild(MatPaginator) public paginator!: MatPaginator;
@ViewChild(MatTable) public table!: MatTable<ProjectMember.AsObject>; @ViewChild(MatTable) public table!: MatTable<ProjectMember.AsObject>;
@ -28,14 +28,14 @@ export class ProjectMembersComponent implements AfterViewInit {
public displayedColumns: string[] = ['select', 'firstname', 'lastname', 'username', 'email', 'roles']; public displayedColumns: string[] = ['select', 'firstname', 'lastname', 'username', 'email', 'roles'];
constructor(private projectService: ProjectService, constructor(private projectService: ProjectService,
private dialog: MatDialog,
private toast: ToastService, private toast: ToastService,
private route: ActivatedRoute) { private route: ActivatedRoute) {
this.route.params.subscribe(params => { this.route.params.subscribe(params => {
this.projectService.GetProjectById(params.projectid).then(project => { this.projectService.GetProjectById(params.projectid).then(project => {
this.project = project.toObject(); this.project = project.toObject();
console.log(this.project);
this.dataSource = new ProjectMembersDataSource(this.projectService); this.dataSource = new ProjectMembersDataSource(this.projectService);
this.dataSource.loadMembers(this.project, 0, 25, 'asc'); this.dataSource.loadMembers(this.project, this.projectType, 0, 25, 'asc');
}); });
}); });
} }
@ -52,6 +52,7 @@ export class ProjectMembersComponent implements AfterViewInit {
private loadMembersPage(): void { private loadMembersPage(): void {
this.dataSource.loadMembers( this.dataSource.loadMembers(
this.project, this.project,
this.projectType,
this.paginator.pageIndex, this.paginator.pageIndex,
this.paginator.pageSize, this.paginator.pageSize,
); );

View File

@ -15,6 +15,7 @@ h1 {
.col { .col {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
.desc { .desc {
font-size: .8rem; font-size: .8rem;
color: #81868a; color: #81868a;
@ -68,3 +69,7 @@ h1 {
} }
} }
} }
tr {
outline: none;
}

View File

@ -87,6 +87,7 @@
}, },
"DATA": { "DATA": {
"STATE": "Status", "STATE": "Status",
"STATE0": "unbekannt",
"STATE1": "aktiv", "STATE1": "aktiv",
"STATE2": "inaktiv", "STATE2": "inaktiv",
"STATE3": "gelöscht", "STATE3": "gelöscht",
@ -325,6 +326,7 @@
"NAME": "Name" "NAME": "Name"
}, },
"ROLE": { "ROLE": {
"KEY":"Key",
"TITLE": "Roles", "TITLE": "Roles",
"DESCRIPTION":"Definieren Sie Rollen, die Sie bei der Erstellung eines Projekt-Grants vergeben können", "DESCRIPTION":"Definieren Sie Rollen, die Sie bei der Erstellung eines Projekt-Grants vergeben können",
"NAME": "Name", "NAME": "Name",
@ -333,7 +335,8 @@
"ACTIONS": "Aktion", "ACTIONS": "Aktion",
"ADDTITLE": "Rolle erstellen", "ADDTITLE": "Rolle erstellen",
"ADDDESCRIPTION": "Geben Sie die Daten für die zu erstellende Rolle ein!", "ADDDESCRIPTION": "Geben Sie die Daten für die zu erstellende Rolle ein!",
"DELETE":"Rolle löschen" "DELETE":"Rolle löschen",
"CREATIONDATE":"Erstelldatum"
}, },
"TABLE": { "TABLE": {
"TOTAL": "Einträge gesamt:", "TOTAL": "Einträge gesamt:",
@ -344,7 +347,7 @@
"ORGNAME":"Org Name", "ORGNAME":"Org Name",
"ORGDOMAIN":"Org Domain", "ORGDOMAIN":"Org Domain",
"STATE":"Status", "STATE":"Status",
"Type":"Typ", "TYPE":"Typ",
"CREATIONDATE":"Erstelldatum", "CREATIONDATE":"Erstelldatum",
"CHANGEDATE":"Letzte Änderung" "CHANGEDATE":"Letzte Änderung"
} }

View File

@ -87,6 +87,7 @@
}, },
"DATA": { "DATA": {
"STATE": "Status", "STATE": "Status",
"STATE0": "unknown",
"STATE1": "active", "STATE1": "active",
"STATE2": "inactive", "STATE2": "inactive",
"STATE3": "deleted", "STATE3": "deleted",
@ -325,6 +326,7 @@
"NAME": "Name" "NAME": "Name"
}, },
"ROLE": { "ROLE": {
"KEY":"Key",
"TITLE": "Roles", "TITLE": "Roles",
"DESCRIPTION":"Define some roles which can be used to create project-grants", "DESCRIPTION":"Define some roles which can be used to create project-grants",
"NAME": "Name", "NAME": "Name",
@ -333,7 +335,8 @@
"ACTIONS": "Actions", "ACTIONS": "Actions",
"ADDTITLE": "Create role", "ADDTITLE": "Create role",
"ADDDESCRIPTION": "Enter the data for the new role!", "ADDDESCRIPTION": "Enter the data for the new role!",
"DELETE":"Delete Role" "DELETE":"Delete Role",
"CREATIONDATE":"Created"
}, },
"TABLE": { "TABLE": {
"TOTAL": "Entries total:", "TOTAL": "Entries total:",
@ -344,7 +347,7 @@
"ORGNAME":"Org Name", "ORGNAME":"Org Name",
"ORGDOMAIN":"Org Domain", "ORGDOMAIN":"Org Domain",
"STATE":"Status", "STATE":"Status",
"Type":"Type", "TYPE":"Type",
"CREATIONDATE":"Created At", "CREATIONDATE":"Created At",
"CHANGEDATE":"Last Modified" "CHANGEDATE":"Last Modified"
} }

View File

@ -1 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M7.07,18.28C7.5,17.38 10.12,16.5 12,16.5C13.88,16.5 16.5,17.38 16.93,18.28C15.57,19.36 13.86,20 12,20C10.14,20 8.43,19.36 7.07,18.28M18.36,16.83C16.93,15.09 13.46,14.5 12,14.5C10.54,14.5 7.07,15.09 5.64,16.83C4.62,15.5 4,13.82 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,13.82 19.38,15.5 18.36,16.83M12,6C10.06,6 8.5,7.56 8.5,9.5C8.5,11.44 10.06,13 12,13C13.94,13 15.5,11.44 15.5,9.5C15.5,7.56 13.94,6 12,6M12,11A1.5,1.5 0 0,1 10.5,9.5A1.5,1.5 0 0,1 12,8A1.5,1.5 0 0,1 13.5,9.5A1.5,1.5 0 0,1 12,11Z" /></svg>

Before

Width:  |  Height:  |  Size: 873 B

View File

@ -1 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M16,17V14H9V10H16V7L21,12L16,17M14,2A2,2 0 0,1 16,4V6H14V4H5V20H14V18H16V20A2,2 0 0,1 14,22H5A2,2 0 0,1 3,20V4A2,2 0 0,1 5,2H14Z" /></svg>

Before

Width:  |  Height:  |  Size: 423 B

View File

@ -11,6 +11,8 @@
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500&amp;display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500&amp;display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet"> <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<link href="https://fonts.googleapis.com/css?family=Rubik&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css?family=Rubik&display=swap" rel="stylesheet">
<link rel="stylesheet"
href="https://maxst.icons8.com/vue-static/landings/line-awesome/line-awesome/1.3.0/css/line-awesome.min.css">
<link rel="manifest" href="manifest.webmanifest"> <link rel="manifest" href="manifest.webmanifest">
<meta name="theme-color" content="#e6768b"> <meta name="theme-color" content="#e6768b">
</head> </head>

View File

@ -207,3 +207,7 @@ body {
border-radius: 0.5rem; border-radius: 0.5rem;
background-color: #212224; background-color: #212224;
} }
i {
font-size: 1.5rem;
}