fix(console): refresh tables, auto refresh emitter, avatar colors (#487)

* refreshtable component

* project grant refresh table

* project role refresh, user grant, i18n

* lint

* auth user mfa table

* auth mfa table

* rm unused 404 page, add mgmt mfa table

* change light accent color

* add actions to mfa table

* user detail mfa table

* clear selection on refresh, bind data length

* member table

* fix padding mfa table

* Update console/src/assets/i18n/en.json

Co-authored-by: Florian Forster <florian@caos.ch>

* Update console/src/assets/i18n/en.json

Co-authored-by: Florian Forster <florian@caos.ch>

* z-index, new colors

* new senf color

Co-authored-by: Florian Forster <florian@caos.ch>
This commit is contained in:
Max Peintner
2020-07-20 15:23:29 +02:00
committed by GitHub
parent 933193855a
commit 117a0d7b19
57 changed files with 789 additions and 711 deletions

View File

@@ -27,7 +27,6 @@
"src/favicon.ico", "src/favicon.ico",
"src/assets", "src/assets",
"src/manifest.webmanifest", "src/manifest.webmanifest",
"src/404.html"
], ],
"styles": [ "styles": [
"src/styles.scss" "src/styles.scss"

View File

@@ -1,23 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>caos console</title>
<script>
sessionStorage.redirect = location.href;
</script>
<meta http-equiv="refresh" content="0;URL='/'"></meta>
</head>
<body>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
</body>
</html>

View File

@@ -47,7 +47,6 @@
</app-accounts-card> </app-accounts-card>
</div> </div>
</mat-toolbar> </mat-toolbar>
<mat-drawer-container class="main-container"> <mat-drawer-container class="main-container">
<mat-drawer #drawer class="sidenav" [mode]="(isHandset$ | async) ? 'over' : 'side'" <mat-drawer #drawer class="sidenav" [mode]="(isHandset$ | async) ? 'over' : 'side'"
[opened]="!(isHandset$ | async)"> [opened]="!(isHandset$ | async)">

View File

@@ -5,7 +5,7 @@
display: flex; display: flex;
height: 60px; height: 60px;
align-items: center; align-items: center;
padding: 0 1rem; padding: 0 1rem;
.logo { .logo {
height: 40px; height: 40px;

View File

@@ -1,7 +1,8 @@
import { animate, group, query, style, transition, trigger } from '@angular/animations'; import { animate, group, query, style, transition, trigger } from '@angular/animations';
import { BreakpointObserver } from '@angular/cdk/layout'; import { BreakpointObserver } from '@angular/cdk/layout';
import { OverlayContainer } from '@angular/cdk/overlay'; import { OverlayContainer } from '@angular/cdk/overlay';
import { Component, HostBinding, OnDestroy, ViewChild } from '@angular/core'; import { ViewportScroller } from '@angular/common';
import { Component, HostBinding, Inject, OnDestroy, ViewChild } from '@angular/core';
import { MatIconRegistry } from '@angular/material/icon'; import { MatIconRegistry } from '@angular/material/icon';
import { MatDrawer } from '@angular/material/sidenav'; import { MatDrawer } from '@angular/material/sidenav';
import { DomSanitizer } from '@angular/platform-browser'; import { DomSanitizer } from '@angular/platform-browser';
@@ -138,6 +139,8 @@ export class AppComponent implements OnDestroy {
private orgSub: Subscription = new Subscription(); private orgSub: Subscription = new Subscription();
constructor( constructor(
public viewPortScroller: ViewportScroller,
@Inject('windowObject') public window: Window,
public translate: TranslateService, public translate: TranslateService,
public authService: AuthService, public authService: AuthService,
private breakpointObserver: BreakpointObserver, private breakpointObserver: BreakpointObserver,

View File

@@ -150,6 +150,7 @@ const authConfig: AuthConfig = {
GrpcService, GrpcService,
AuthService, AuthService,
AuthUserService, AuthUserService,
{ provide: 'windowObject', useValue: window },
], ],
bootstrap: [AppComponent], bootstrap: [AppComponent],
}) })

View File

@@ -1,5 +1,5 @@
<div class="avatar-circle dontcloseonclick" <div class="avatar-circle dontcloseonclick"
[ngStyle]="{'height': size+'px', 'width': size+'px', 'fontSize': fontSize+'px', 'background-color': color}" [ngStyle]="{'height': size+'px', 'width': size+'px', 'fontSize': (fontSize-1)+'px', 'background-color': color}"
[ngClass]="{'active': active}"> [ngClass]="{'active': active}">
{{credentials}} {{credentials}}
</div> </div>

View File

@@ -30,22 +30,22 @@ export class AvatarComponent implements OnInit {
getColor(userName: string): string { getColor(userName: string): string {
const colors = [ const colors = [
'#e51c23', '#B44D51',
'#e91e63', '#B75073',
'#9c27b0', '#84498E',
'#673ab7', '#705998',
'#3f51b5', '#5C6598',
'#5677fc', '#7F90D3',
'#03a9f4', '#3E93B9',
'#00bcd4', '#3494A0',
'#009688', '#25716A',
'#259b24', '#427E41',
'#8bc34a', '#89A568',
'#afb42b', '#90924D',
'#ff9800', '#E2B032',
'#ff5722', '#C97358',
'#795548', '#6D5B54',
'#607d8b', '#6B7980',
]; ];
let hash = 0; let hash = 0;

View File

@@ -4,8 +4,8 @@
<div class="people"> <div class="people">
<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; index as i">
<div (click)="showDetail()" class="avatar-circle" <div (click)="showDetail()" class="avatar-circle" [ngStyle]="{'z-index': 100 - i}"
matTooltip="{{ member.email }} | {{member.rolesList?.join(' ')}}"> matTooltip="{{ member.email }} | {{member.rolesList?.join(' ')}}">
<app-avatar *ngIf="member && member.firstName && member.lastName; else thumbavatar" <app-avatar *ngIf="member && member.firstName && member.lastName; else thumbavatar"
class="avatar dontcloseonclick" [name]="member.firstName + ' '+ member.lastName" class="avatar dontcloseonclick" [name]="member.firstName + ' '+ member.lastName"

View File

@@ -28,18 +28,18 @@
margin-left: 1rem; margin-left: 1rem;
display: flex; display: flex;
align-items: center; align-items: center;
transition: all .15s ease-in-out;
.avatar-circle { .avatar-circle {
transition: all .3s ease-in-out;
float: left; float: left;
margin: 0 8px 0 -15px; margin: 0 8px 0 -15px;
height: 32px; height: 32px;
width: 32px; width: 32px;
border-radius: 50%; border-radius: 50%;
&:not(:first-child) { -webkit-box-shadow: 2px 0px 7px -1px rgba(33,34,36,.5);
-webkit-box-shadow: -9px 0px 20px -6px rgba(0,0,0,0.75); -moz-box-shadow: 2px 0px 7px -1px rgba(33,34,36,.5);
-moz-box-shadow: -9px 0px 20px -6px rgba(0,0,0,0.75); box-shadow: 2px 0px 7px -1px rgba(33,34,36,.5);
box-shadow: -9px 0px 20px -6px rgba(0,0,0,0.75);
}
} }
.add-img { .add-img {

View File

@@ -1,44 +1,32 @@
<div class="max-width-container"> <div class="container">
<div class="container"> <div class="left">
<div class="left"> <a *ngIf="project" [routerLink]="[ '/projects', project.projectId]" mat-icon-button>
<a *ngIf="project" [routerLink]="[ '/projects', project.projectId]" mat-icon-button> <mat-icon class="icon">arrow_back</mat-icon>
<mat-icon class="icon">arrow_back</mat-icon> </a>
</a> </div>
<div class="right">
<div class="head">
<h1>{{projectName}} {{ 'PROJECT.MEMBER.TITLE' | translate }}</h1>
<p class="desc">{{ 'PROJECT.MEMBER.DESCRIPTION' | translate }}</p>
</div> </div>
<div class="right">
<div class="head">
<h1>{{projectName}} {{ 'PROJECT.MEMBER.TITLE' | translate }}</h1>
<p class="desc">{{ 'PROJECT.MEMBER.DESCRIPTION' | translate }}</p>
</div>
<div class="table-header-row" *ngIf="project"> <app-refresh-table *ngIf="project" (refreshed)="changePage()" [dataSize]="dataSource.totalResult"
<div class="col"> [selection]="selection">
<ng-container *ngIf="!selection.hasValue()"> <ng-template appHasRole actions
<span class="desc">{{'ORG_DETAIL.TABLE.TOTAL' | translate}}</span> [appHasRole]="['project.member.delete:' + project.projectId, 'project.member.delete']">
<span class="count">{{dataSource?.membersSubject.value.length}}</span> <button (click)="removeProjectMemberSelection()" color="warn"
</ng-container> matTooltip="{{'ORG_DETAIL.TABLE.DELETE' | translate}}" class="icon-button" mat-icon-button
<ng-container *ngIf="selection.hasValue()"> *ngIf="selection.hasValue()">
<span class="desc">{{'ORG_DETAIL.TABLE.SELECTION' | translate}}</span> <i class="las la-trash"></i>
<span class="count">{{selection?.selected?.length}}</span> </button>
</ng-container> </ng-template>
</div> <ng-template appHasRole actions
<span class="fill-space"></span> [appHasRole]="['project.member.write:'+project.projectId,'project.member.write']">
<ng-template appHasRole <a color="primary" [disabled]="disabled" class="add-button" (click)="openAddMember()" color="primary"
[appHasRole]="['project.member.delete:' + project.projectId, 'project.member.delete']"> mat-raised-button>
<button (click)="removeProjectMemberSelection()" color="warn" <mat-icon class="icon">add</mat-icon>{{ 'ACTIONS.NEW' | translate }}
matTooltip="{{'ORG_DETAIL.TABLE.DELETE' | translate}}" class="icon-button" mat-icon-button </a>
*ngIf="selection.hasValue()"> </ng-template>
<i class="las la-trash"></i>
</button>
</ng-template>
<ng-template appHasRole
[appHasRole]="['project.member.write:'+project.projectId,'project.member.write']">
<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>
</ng-template>
</div>
<div class="table-wrapper"> <div class="table-wrapper">
<div class="spinner-container" *ngIf="dataSource?.loading$ | async"> <div class="spinner-container" *ngIf="dataSource?.loading$ | async">
@@ -118,6 +106,6 @@
(page)="changePage($event)"> (page)="changePage($event)">
</mat-paginator> </mat-paginator>
</div> </div>
</div> </app-refresh-table>
</div> </div>
</div> </div>

View File

@@ -16,6 +16,7 @@
.right { .right {
flex: 1; flex: 1;
padding-top: 1rem; padding-top: 1rem;
padding-right: 1rem;
.head { .head {
display: flex; display: flex;
@@ -42,33 +43,12 @@
} }
} }
.table-header-row { .icon-button {
display: flex; margin-right: .5rem;
align-items: center; }
.col { .add-button {
display: flex; border-radius: .5rem;
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 { .table-wrapper {

View File

@@ -170,7 +170,13 @@ export class ProjectMembersComponent {
} }
} }
public changePage(event: PageEvent): void { public changePage(event?: PageEvent): void {
this.dataSource.loadMembers(this.project.projectId, this.projectType, event.pageIndex, event.pageSize, this.grantId); this.dataSource.loadMembers(
this.project.projectId,
this.projectType,
event?.pageIndex ?? this.paginator.pageIndex,
event?.pageSize ?? this.paginator.pageSize,
this.grantId,
);
} }
} }

View File

@@ -5,6 +5,7 @@ import { MatAutocompleteModule } from '@angular/material/autocomplete';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox'; import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatChipsModule } from '@angular/material/chips'; import { MatChipsModule } from '@angular/material/chips';
import { MatDialogModule } from '@angular/material/dialog';
import { MatFormFieldModule } from '@angular/material/form-field'; import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon'; import { MatIconModule } from '@angular/material/icon';
import { MatPaginatorModule } from '@angular/material/paginator'; import { MatPaginatorModule } from '@angular/material/paginator';
@@ -17,6 +18,7 @@ import { TranslateModule } from '@ngx-translate/core';
import { HasRoleModule } from 'src/app/directives/has-role/has-role.module'; import { HasRoleModule } from 'src/app/directives/has-role/has-role.module';
import { HasRolePipeModule } from 'src/app/pipes/has-role-pipe.module'; import { HasRolePipeModule } from 'src/app/pipes/has-role-pipe.module';
import { RefreshTableModule } from '../refresh-table/refresh-table.module';
import { ProjectMembersRoutingModule } from './project-members-routing.module'; import { ProjectMembersRoutingModule } from './project-members-routing.module';
import { ProjectMembersComponent } from './project-members.component'; import { ProjectMembersComponent } from './project-members.component';
@@ -43,6 +45,8 @@ import { ProjectMembersComponent } from './project-members.component';
FormsModule, FormsModule,
TranslateModule, TranslateModule,
HasRolePipeModule, HasRolePipeModule,
RefreshTableModule,
MatDialogModule,
], ],
}) })
export class ProjectMembersModule { } export class ProjectMembersModule { }

View File

@@ -1,79 +1,70 @@
<div class="table-header-row" *ngIf="projectId"> <app-refresh-table *ngIf="projectId" (refreshed)="refreshPage()" [dataSize]="dataSource.totalResult"
<div class="col"> [selection]="selection">
<ng-container *ngIf="!selection.hasValue()"> <ng-template appHasRole [appHasRole]="['project.role.delete']" actions>
<span class="desc">{{'ORG_DETAIL.TABLE.TOTAL' | translate}}</span> <button color="warn" class="icon-button" [disabled]="disabled"
<span class="count">{{dataSource?.rolesSubject.value.length}}</span> matTooltip="{{'PROJECT.ROLE.DELETE' | translate}}" (click)="deleteSelectedRoles()" mat-icon-button
</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.role.delete']">
<button color="warn" [disabled]="disabled" matTooltip="{{'PROJECT.ROLE.DELETE' | translate}}"
class="icon-button" (click)="deleteSelectedRoles()" mat-icon-button
*ngIf="selection.hasValue() && actionsVisible"> *ngIf="selection.hasValue() && actionsVisible">
<i class="las la-trash"></i> <i class="las la-trash"></i>
</button> </button>
</ng-template> </ng-template>
<ng-template appHasRole [appHasRole]="['project.role.write:' + projectId, 'project.role.write']"> <ng-template appHasRole [appHasRole]="['project.role.write:' + projectId, 'project.role.write']" actions>
<a *ngIf="actionsVisible" [disabled]="disabled" class="add-button" <a *ngIf="actionsVisible" [disabled]="disabled" class="rounded-button"
[routerLink]="[ '/projects', projectId, 'roles', 'create']" color="primary" mat-raised-button> [routerLink]="[ '/projects', projectId, 'roles', 'create']" color="primary" mat-raised-button>
<mat-icon class="icon">add</mat-icon>{{ 'ACTIONS.NEW' | translate }} <mat-icon class="icon">add</mat-icon>{{ 'ACTIONS.NEW' | translate }}
</a> </a>
</ng-template> </ng-template>
</div>
<div class="table-wrapper"> <div class="table-wrapper">
<div class="spinner-container" *ngIf="dataSource.loading$ | async"> <div class="spinner-container" *ngIf="dataSource.loading$ | async">
<mat-spinner diameter="50"></mat-spinner> <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="key">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.ROLE.KEY' | translate }} </th>
<td (click)="openDetailDialog(role)" mat-cell *matCellDef="let role"> {{role.key}} </td>
</ng-container>
<ng-container matColumnDef="displayname">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.ROLE.DISPLAY_NAME' | translate }} </th>
<td (click)="openDetailDialog(role)" mat-cell *matCellDef="let role"> {{role.displayName}} </td>
</ng-container>
<ng-container matColumnDef="group">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.ROLE.GROUP' | translate }} </th>
<td mat-cell *matCellDef="let role">
<span class="role app-label" *ngIf="role.group" (click)="selectAllOfGroup(role.group)"
[matTooltip]="'PROJECT.ROLE.SELECTGROUPTOOLTIP' | translate: role">{{role.group}}</span>
</td>
</ng-container>
<ng-container matColumnDef="creationDate">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.ROLE.CREATIONDATE' | translate }} </th>
<td (click)="openDetailDialog(role)" mat-cell *matCellDef="let role">
<span>{{role.creationDate | timestampToDate | date: 'dd. MMM, HH:mm' }}</span>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr 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> </div>
</app-refresh-table>
<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="key">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.ROLE.KEY' | translate }} </th>
<td (click)="openDetailDialog(role)" mat-cell *matCellDef="let role"> {{role.key}} </td>
</ng-container>
<ng-container matColumnDef="displayname">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.ROLE.DISPLAY_NAME' | translate }} </th>
<td (click)="openDetailDialog(role)" mat-cell *matCellDef="let role"> {{role.displayName}} </td>
</ng-container>
<ng-container matColumnDef="group">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.ROLE.GROUP' | translate }} </th>
<td mat-cell *matCellDef="let role">
<span class="role app-label" *ngIf="role.group" (click)="selectAllOfGroup(role.group)"
[matTooltip]="'PROJECT.ROLE.SELECTGROUPTOOLTIP' | translate: role">{{role.group}}</span>
</td>
</ng-container>
<ng-container matColumnDef="creationDate">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.ROLE.CREATIONDATE' | translate }} </th>
<td (click)="openDetailDialog(role)" mat-cell *matCellDef="let role">
<span>{{role.creationDate | timestampToDate | date: 'dd. MMM, HH:mm' }}</span>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr 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>

View File

@@ -1,31 +1,10 @@
.table-header-row { .rounded-button {
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; border-radius: .5rem;
} }
.icon-button {
margin-right: .5rem;
} }
.table-wrapper { .table-wrapper {

View File

@@ -30,10 +30,11 @@ export class ProjectRolesComponent implements AfterViewInit, OnInit {
/** 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', 'key', 'displayname', 'group', 'creationDate']; public displayedColumns: string[] = ['select', 'key', 'displayname', 'group', 'creationDate'];
constructor(private projectService: ProjectService, private toast: ToastService, private dialog: MatDialog) { } constructor(private projectService: ProjectService, private toast: ToastService, private dialog: MatDialog) {
this.dataSource = new ProjectRolesDataSource(this.projectService);
}
public ngOnInit(): void { public ngOnInit(): void {
this.dataSource = new ProjectRolesDataSource(this.projectService);
this.dataSource.loadRoles(this.projectId, 0, 25, 'asc'); this.dataSource.loadRoles(this.projectId, 0, 25, 'asc');
this.selection.changed.subscribe(() => { this.selection.changed.subscribe(() => {
@@ -119,4 +120,8 @@ export class ProjectRolesComponent implements AfterViewInit, OnInit {
width: '400px', width: '400px',
}); });
} }
public refreshPage(): void {
this.dataSource.loadRoles(this.projectId, this.paginator.pageIndex, this.paginator.pageSize);
}
} }

View File

@@ -18,6 +18,7 @@ import { HasRoleModule } from 'src/app/directives/has-role/has-role.module';
import { HasRolePipeModule } from 'src/app/pipes/has-role-pipe.module'; import { HasRolePipeModule } from 'src/app/pipes/has-role-pipe.module';
import { TimestampToDatePipeModule } from 'src/app/pipes/timestamp-to-date-pipe.module'; import { TimestampToDatePipeModule } from 'src/app/pipes/timestamp-to-date-pipe.module';
import { RefreshTableModule } from '../refresh-table/refresh-table.module';
import { ProjectRoleDetailComponent } from './project-role-detail/project-role-detail.component'; import { ProjectRoleDetailComponent } from './project-role-detail/project-role-detail.component';
import { ProjectRolesComponent } from './project-roles.component'; import { ProjectRolesComponent } from './project-roles.component';
@@ -44,6 +45,7 @@ import { ProjectRolesComponent } from './project-roles.component';
TranslateModule, TranslateModule,
MatMenuModule, MatMenuModule,
TimestampToDatePipeModule, TimestampToDatePipeModule,
RefreshTableModule,
], ],
exports: [ exports: [
ProjectRolesComponent, ProjectRolesComponent,

View File

@@ -0,0 +1,18 @@
<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">{{dataSize}}</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 mat-icon-button (click)="emitRefresh()" class="icon-button" matTooltip="{{'ACTIONS.REFRESH' | translate}}">
<mat-icon>refresh</mat-icon>
</button>
<ng-content select="[actions]"></ng-content>
</div>
<ng-content></ng-content>

View File

@@ -0,0 +1,26 @@
.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;
}
}

View File

@@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { RefreshTableComponent } from './refresh-table.component';
describe('RefreshTableComponent', () => {
let component: RefreshTableComponent;
let fixture: ComponentFixture<RefreshTableComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [RefreshTableComponent],
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(RefreshTableComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,46 @@
import { animate, animation, keyframes, style, transition, trigger, useAnimation } from '@angular/animations';
import { SelectionModel } from '@angular/cdk/collections';
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
const rotate = animation([
animate(
'{{time}} cubic-bezier(0.785, 0.135, 0.15, 0.86)',
keyframes([
style({
transform: 'rotate(0deg)',
}),
style({
transform: 'rotate(360deg)',
}),
]),
),
]);
@Component({
selector: 'app-refresh-table',
templateUrl: './refresh-table.component.html',
styleUrls: ['./refresh-table.component.scss'],
animations: [
trigger('rotate', [
transition('* => *', [useAnimation(rotate, { params: { time: '1s' } })]),
]),
],
})
export class RefreshTableComponent implements OnInit {
@Input() public selection: SelectionModel<any> = new SelectionModel<any>(true, []);
@Input() public dataSize: number = 0;
@Input() public emitRefreshAfterTimeoutInMs: number = 0;
@Output() public refreshed: EventEmitter<void> = new EventEmitter();
ngOnInit(): void {
if (this.emitRefreshAfterTimeoutInMs) {
setTimeout(() => {
this.emitRefresh();
}, this.emitRefreshAfterTimeoutInMs);
}
}
emitRefresh(): void {
this.selection.clear();
return this.refreshed.emit();
}
}

View File

@@ -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 { MatIconModule } from '@angular/material/icon';
import { MatTooltipModule } from '@angular/material/tooltip';
import { TranslateModule } from '@ngx-translate/core';
import { RefreshTableComponent } from './refresh-table.component';
@NgModule({
declarations: [RefreshTableComponent],
imports: [
CommonModule,
MatButtonModule,
MatIconModule,
TranslateModule,
FormsModule,
MatTooltipModule,
],
exports: [
RefreshTableComponent,
],
})
export class RefreshTableModule { }

View File

@@ -1,148 +1,119 @@
<div class="table-header-row"> <app-refresh-table (refreshed)="changePage()" [dataSize]="dataSource.totalResult" [selection]="selection">
<div class="col"> <button color="warn" matTooltip="{{'GRANTS.DELETE' | translate}}" class="icon-button" mat-icon-button actions
<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>
<button color="warn" matTooltip="{{'GRANTS.DELETE' | translate}}" class="icon-button" mat-icon-button
(click)="deleteGrantSelection()" *ngIf="selection.hasValue() && allowDelete"> (click)="deleteGrantSelection()" *ngIf="selection.hasValue() && allowDelete">
<i class="las la-trash"></i> <i class="las la-trash"></i>
</button> </button>
<a *ngIf="allowCreate && context !== UserGrantContext.USER" matTooltip="{{'GRANTS.ADD' | translate}}" <a *ngIf="allowCreate && context !== UserGrantContext.USER" matTooltip="{{'GRANTS.ADD' | translate}}" actions
color="primary" class="add-button" color="primary" mat-raised-button [routerLink]="routerLink"> color="primary" class="add-button" color="primary" mat-raised-button [routerLink]="routerLink">
<mat-icon class="icon">add</mat-icon>{{ 'GRANTS.ADD_BTN' | translate }} <mat-icon class="icon">add</mat-icon>{{ 'GRANTS.ADD_BTN' | translate }}
</a> </a>
</div>
<div class="table-wrapper"> <div class="table-wrapper">
<div class="spinner-container" *ngIf="dataSource.loading$ | async"> <div class="spinner-container" *ngIf="dataSource.loading$ | async">
<mat-spinner diameter="50"></mat-spinner> <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)">
<app-avatar *ngIf="row && (row?.displayName || (row.firstName && row.lastName))" class="avatar"
[name]="row.displayName ? row.displayName : (row.firstName + ' '+ row.lastName)"
[size]="32">
</app-avatar>
</mat-checkbox>
</td>
</ng-container>
<ng-container matColumnDef="user">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.GRANT.USER' | translate }} </th>
<td class="pointer" mat-cell *matCellDef="let grant">
{{grant?.firstName}} {{grant?.lastName}}</td>
</ng-container>
<ng-container matColumnDef="org">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.GRANT.GRANTEDORGDOMAIN' | translate }} </th>
<td class="pointer" mat-cell *matCellDef="let grant">
{{grant.orgName}} </td>
</ng-container>
<ng-container matColumnDef="projectId">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.GRANT.PROJECTNAME' | translate }} </th>
<td class="pointer" mat-cell *matCellDef="let grant">
{{grant.projectName}} </td>
</ng-container>
<ng-container matColumnDef="creationDate">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.GRANT.CREATIONDATE' | translate }} </th>
<td 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 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 mat-cell *matCellDef="let grant; let i = index">
<ng-container *ngIf="context === UserGrantContext.USER">
<span class="no-roles"
*ngIf="grant.roleKeysList?.length === 0">{{'PROJECT.GRANT.NOROLES' | translate}}</span>
<span
*ngFor="let role of grant.roleKeysList">{{ (role.length>8)? (role | slice:0:8)+'..':(role) }}</span>
</ng-container>
<ng-container *ngIf="context === UserGrantContext.OWNED_PROJECT">
<ng-container *ngIf="loadedProjectId !== grant.projectId">
<span class="role app-label"
*ngFor="let role of grant.roleKeysList">{{ (role.length>6)? (role | slice:0:6)+'..':(role) }}</span>
<button mat-icon-button (click)="getProjectRoleOptions(grant.projectId)"
matTooltip="{{'ACTIONS.CHANGE' | translate}}">
<i class="las la-edit"></i>
</button>
</ng-container>
<mat-form-field class="form-field" appearance="outline"
*ngIf="loadedProjectId === grant.projectId">
<mat-label>{{ 'PROJECT.GRANT.ROLENAMESLIST' | translate }}</mat-label>
<mat-select [(ngModel)]="grant.roleKeysList" multiple [disabled]="allowCreate == false"
(selectionChange)="updateRoles(grant, $event)">
<mat-option *ngFor="let role of projectRoleOptions" [value]="role.key">
{{role.key}}
</mat-option>
</mat-select>
</mat-form-field>
</ng-container>
<ng-container *ngIf="context === UserGrantContext.GRANTED_PROJECT">
<mat-form-field class="form-field" appearance="outline">
<mat-label>{{ 'PROJECT.GRANT.ROLENAMESLIST' | translate }}</mat-label>
<mat-select [(ngModel)]="grant.roleKeysList" multiple [disabled]="allowCreate == false"
(selectionChange)="updateRoles(grant, $event)">
<mat-option *ngFor="let role of grantRoleOptions" [value]="role">
{{role}}
</mat-option>
</mat-select>
</mat-form-field>
</ng-container>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;" class="element-row">
</tr>
</table>
<mat-paginator #paginator [length]="dataSource.totalResult" [pageSize]="50" [length]="dataSource.totalResult"
[pageSizeOptions]="[2, 3, 25, 50, 100, 250]" (page)="changePage($event)">
</mat-paginator>
</div> </div>
<table mat-table multiTemplateDataRows class="full-width-table" aria-label="Elements" [dataSource]="dataSource"> </app-refresh-table>
<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)">
<app-avatar *ngIf="row && (row?.displayName || (row.firstName && row.lastName))" class="avatar"
[name]="row.displayName ? row.displayName : (row.firstName + ' '+ row.lastName)" [size]="32">
</app-avatar>
</mat-checkbox>
</td>
</ng-container>
<ng-container matColumnDef="user">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.GRANT.USER' | translate }} </th>
<td class="pointer" mat-cell *matCellDef="let grant">
{{grant?.firstName}} {{grant?.lastName}}</td>
</ng-container>
<ng-container matColumnDef="org">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.GRANT.GRANTEDORGDOMAIN' | translate }} </th>
<td class="pointer" mat-cell *matCellDef="let grant">
{{grant.orgName}} </td>
</ng-container>
<ng-container matColumnDef="projectId">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.GRANT.PROJECTNAME' | translate }} </th>
<td class="pointer" mat-cell *matCellDef="let grant">
{{grant.projectName}} </td>
</ng-container>
<ng-container matColumnDef="creationDate">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.GRANT.CREATIONDATE' | translate }} </th>
<td 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 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 mat-cell *matCellDef="let grant; let i = index">
<ng-container *ngIf="context === UserGrantContext.USER">
<span class="no-roles"
*ngIf="grant.roleKeysList?.length === 0">{{'PROJECT.GRANT.NOROLES' | translate}}</span>
<span
*ngFor="let role of grant.roleKeysList">{{ (role.length>8)? (role | slice:0:8)+'..':(role) }}</span>
</ng-container>
<!-- <ng-container *ngIf="context === UserGrantContext.USER">
<ng-container *ngIf="loadedGrantId !== grant.grantId">
<span class="role app-label" *ngFor="let role of grant.roleKeysList">{{role}}</span>
<button mat-icon-button (click)="getGrantRoleOptions(grant.grantId, grant.projectId)"
matTooltip="{{'ACTIONS.CHANGE' | translate}}">
<i class="las la-edit"></i>
</button>
</ng-container>
<mat-form-field class="form-field" appearance="outline" *ngIf="loadedGrantId === grant.grantId">
<mat-label>{{ 'PROJECT.GRANT.ROLENAMESLIST' | translate }}</mat-label>
<mat-select [(ngModel)]="grant.roleKeysList" multiple [disabled]="allowCreate == false"
(selectionChange)="updateRoles(grant, $event)">
<mat-option *ngFor="let role of grantRoleOptions" [value]="role">
{{role}}
</mat-option>
</mat-select>
</mat-form-field>
</ng-container> -->
<ng-container *ngIf="context === UserGrantContext.OWNED_PROJECT">
<ng-container *ngIf="loadedProjectId !== grant.projectId">
<span class="role app-label"
*ngFor="let role of grant.roleKeysList">{{ (role.length>6)? (role | slice:0:6)+'..':(role) }}</span>
<button mat-icon-button (click)="getProjectRoleOptions(grant.projectId)"
matTooltip="{{'ACTIONS.CHANGE' | translate}}">
<i class="las la-edit"></i>
</button>
</ng-container>
<mat-form-field class="form-field" appearance="outline" *ngIf="loadedProjectId === grant.projectId">
<mat-label>{{ 'PROJECT.GRANT.ROLENAMESLIST' | translate }}</mat-label>
<mat-select [(ngModel)]="grant.roleKeysList" multiple [disabled]="allowCreate == false"
(selectionChange)="updateRoles(grant, $event)">
<mat-option *ngFor="let role of projectRoleOptions" [value]="role.key">
{{role.key}}
</mat-option>
</mat-select>
</mat-form-field>
</ng-container>
<ng-container *ngIf="context === UserGrantContext.GRANTED_PROJECT">
<mat-form-field class="form-field" appearance="outline">
<mat-label>{{ 'PROJECT.GRANT.ROLENAMESLIST' | translate }}</mat-label>
<mat-select [(ngModel)]="grant.roleKeysList" multiple [disabled]="allowCreate == false"
(selectionChange)="updateRoles(grant, $event)">
<mat-option *ngFor="let role of grantRoleOptions" [value]="role">
{{role}}
</mat-option>
</mat-select>
</mat-form-field>
</ng-container>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;" class="element-row">
</tr>
</table>
<mat-paginator #paginator [length]="dataSource.totalResult" [pageSize]="50" [length]="dataSource.totalResult"
[pageSizeOptions]="[2, 3, 25, 50, 100, 250]" (page)="changePage($event)">
</mat-paginator>
</div>

View File

@@ -1,31 +1,6 @@
.table-header-row { .add-button {
display: flex; border-radius: .5rem;
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 { .table-wrapper {

View File

@@ -176,11 +176,16 @@ export class UserGrantsComponent implements OnInit, AfterViewInit {
}); });
} }
public changePage(event: PageEvent): void { public changePage(event?: PageEvent): void {
this.dataSource.loadGrants(this.context, event.pageIndex, event.pageSize, { this.dataSource.loadGrants(
projectId: this.projectId, this.context,
grantId: this.grantId, event?.pageIndex ?? this.paginator.pageIndex,
userId: this.userId, event?.pageSize ?? this.paginator.pageSize,
}); {
projectId: this.projectId,
grantId: this.grantId,
userId: this.userId,
},
);
} }
} }

View File

@@ -17,6 +17,7 @@ import { HasRolePipeModule } from 'src/app/pipes/has-role-pipe.module';
import { TimestampToDatePipeModule } from 'src/app/pipes/timestamp-to-date-pipe.module'; import { TimestampToDatePipeModule } from 'src/app/pipes/timestamp-to-date-pipe.module';
import { AvatarModule } from '../avatar/avatar.module'; import { AvatarModule } from '../avatar/avatar.module';
import { RefreshTableModule } from '../refresh-table/refresh-table.module';
import { UserGrantsComponent } from './user-grants.component'; import { UserGrantsComponent } from './user-grants.component';
@@ -41,6 +42,7 @@ import { UserGrantsComponent } from './user-grants.component';
TranslateModule, TranslateModule,
HasRolePipeModule, HasRolePipeModule,
TimestampToDatePipeModule, TimestampToDatePipeModule,
RefreshTableModule,
], ],
exports: [ exports: [
UserGrantsComponent, UserGrantsComponent,

View File

@@ -4,8 +4,8 @@
<div class="people"> <div class="people">
<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; index as i">
<div (click)="showDetail()" class="avatar-circle" <div (click)="showDetail()" class="avatar-circle" [ngStyle]="{'z-index': 100 - i}"
matTooltip="{{ member.email }} | {{member.rolesList?.join(' ')}}"> matTooltip="{{ member.email }} | {{member.rolesList?.join(' ')}}">
<app-avatar *ngIf="member && (member.displayName || (member.firstName && member.lastName))" <app-avatar *ngIf="member && (member.displayName || (member.firstName && member.lastName))"
class="avatar dontcloseonclick" class="avatar dontcloseonclick"

View File

@@ -35,11 +35,9 @@
height: 32px; height: 32px;
width: 32px; width: 32px;
border-radius: 50%; border-radius: 50%;
&:not(:first-child) { -webkit-box-shadow: 2px 0px 7px -1px rgba(33,34,36,.5);
-webkit-box-shadow: -9px 0px 20px -6px rgba(0,0,0,0.75); -moz-box-shadow: 2px 0px 7px -1px rgba(33,34,36,.5);
-moz-box-shadow: -9px 0px 20px -6px rgba(0,0,0,0.75); box-shadow: 2px 0px 7px -1px rgba(33,34,36,.5);
box-shadow: -9px 0px 20px -6px rgba(0,0,0,0.75);
}
} }
.add-img { .add-img {

View File

@@ -4,9 +4,10 @@
<div class="people"> <div class="people">
<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; index as i">
<div (click)="showDetail()" class="avatar-circle" <div (click)="showDetail()" class="avatar-circle"
matTooltip="{{ member.email }} | {{member.rolesList?.join(' ')}}"> matTooltip="{{ member.email }} | {{member.rolesList?.join(' ')}}"
[ngStyle]="{'z-index': 100 - i}">
<app-avatar *ngIf="member && (member.displayName || (member.firstName && member.lastName))" <app-avatar *ngIf="member && (member.displayName || (member.firstName && member.lastName))"
class="avatar dontcloseonclick" class="avatar dontcloseonclick"
[name]="member.displayName ? member.displayName : (member.firstName + ' '+ member.lastName)" [name]="member.displayName ? member.displayName : (member.firstName + ' '+ member.lastName)"

View File

@@ -29,17 +29,15 @@
display: flex; display: flex;
align-items: center; align-items: center;
.avatar-img, .avatar-circle { .avatar-circle {
float: left; float: left;
margin: 0 8px 0 -15px; margin: 0 8px 0 -15px;
height: 32px; height: 32px;
width: 32px; width: 32px;
border-radius: 50%; border-radius: 50%;
&:not(:first-child) { -webkit-box-shadow: 2px 0px 7px -1px rgba(33,34,36,.5);
-webkit-box-shadow: -9px 0px 20px -6px rgba(0,0,0,0.75); -moz-box-shadow: 2px 0px 7px -1px rgba(33,34,36,.5);
-moz-box-shadow: -9px 0px 20px -6px rgba(0,0,0,0.75); box-shadow: 2px 0px 7px -1px rgba(33,34,36,.5);
box-shadow: -9px 0px 20px -6px rgba(0,0,0,0.75);
}
} }
.add-img { .add-img {

View File

@@ -5,7 +5,7 @@ import { MatTable } from '@angular/material/table';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { BehaviorSubject, from, Observable, of } from 'rxjs'; import { BehaviorSubject, from, Observable, of } from 'rxjs';
import { catchError, finalize, map } from 'rxjs/operators'; import { catchError, finalize, map } from 'rxjs/operators';
import { Org, OrgMember, OrgMemberView, OrgState, User } from 'src/app/proto/generated/management_pb'; import { Org, OrgMemberView, OrgState, User } from 'src/app/proto/generated/management_pb';
import { OrgService } from 'src/app/services/org.service'; import { OrgService } from 'src/app/services/org.service';
import { ToastService } from 'src/app/services/toast.service'; import { ToastService } from 'src/app/services/toast.service';
@@ -23,7 +23,7 @@ export class OrgContributorsComponent implements OnInit {
@Input() public org!: Org.AsObject; @Input() public org!: Org.AsObject;
@Input() public disabled: boolean = false; @Input() public disabled: boolean = false;
@ViewChild(MatPaginator) public paginator!: MatPaginator; @ViewChild(MatPaginator) public paginator!: MatPaginator;
@ViewChild(MatTable) public table!: MatTable<OrgMember.AsObject>; @ViewChild(MatTable) public table!: MatTable<OrgMemberView.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', 'firstname', 'lastname', 'username', 'email', 'roles']; public displayedColumns: string[] = ['select', 'firstname', 'lastname', 'username', 'email', 'roles'];

View File

@@ -17,6 +17,7 @@ import { ChangesModule } from 'src/app/modules/changes/changes.module';
import { MetaLayoutModule } from 'src/app/modules/meta-layout/meta-layout.module'; import { MetaLayoutModule } from 'src/app/modules/meta-layout/meta-layout.module';
import { ProjectContributorsModule } from 'src/app/modules/project-contributors/project-contributors.module'; import { ProjectContributorsModule } from 'src/app/modules/project-contributors/project-contributors.module';
import { ProjectRolesModule } from 'src/app/modules/project-roles/project-roles.module'; import { ProjectRolesModule } from 'src/app/modules/project-roles/project-roles.module';
import { RefreshTableModule } from 'src/app/modules/refresh-table/refresh-table.module';
import { UserGrantsModule } from 'src/app/modules/user-grants/user-grants.module'; import { UserGrantsModule } from 'src/app/modules/user-grants/user-grants.module';
import { WarnDialogModule } from 'src/app/modules/warn-dialog/warn-dialog.module'; import { WarnDialogModule } from 'src/app/modules/warn-dialog/warn-dialog.module';
import { HasRolePipeModule } from 'src/app/pipes/has-role-pipe.module'; import { HasRolePipeModule } from 'src/app/pipes/has-role-pipe.module';
@@ -59,6 +60,7 @@ import { ProjectGrantsComponent } from './project-grants/project-grants.componen
MatProgressSpinnerModule, MatProgressSpinnerModule,
ChangesModule, ChangesModule,
MetaLayoutModule, MetaLayoutModule,
RefreshTableModule,
], ],
}) })
export class OwnedProjectDetailModule { } export class OwnedProjectDetailModule { }

View File

@@ -10,6 +10,7 @@ import { ProjectService } from 'src/app/services/project.service';
* (including sorting, pagination, and filtering). * (including sorting, pagination, and filtering).
*/ */
export class ProjectGrantsDataSource extends DataSource<ProjectGrant.AsObject> { export class ProjectGrantsDataSource extends DataSource<ProjectGrant.AsObject> {
public totalResult: number = 0;
public grantsSubject: BehaviorSubject<ProjectGrant.AsObject[]> = new BehaviorSubject<ProjectGrant.AsObject[]>([]); public grantsSubject: BehaviorSubject<ProjectGrant.AsObject[]> = new BehaviorSubject<ProjectGrant.AsObject[]>([]);
private loadingSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false); private loadingSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
public loading$: Observable<boolean> = this.loadingSubject.asObservable(); public loading$: Observable<boolean> = this.loadingSubject.asObservable();
@@ -24,6 +25,7 @@ export class ProjectGrantsDataSource extends DataSource<ProjectGrant.AsObject> {
this.loadingSubject.next(true); this.loadingSubject.next(true);
from(this.projectService.SearchProjectGrants(projectId, pageSize, offset)).pipe( from(this.projectService.SearchProjectGrants(projectId, pageSize, offset)).pipe(
map(resp => { map(resp => {
this.totalResult = resp.toObject().totalResult;
return resp.toObject().resultList; return resp.toObject().resultList;
}), }),
catchError(() => of([])), catchError(() => of([])),

View File

@@ -1,91 +1,84 @@
<div class="table-header-row" *ngIf="projectId"> <app-refresh-table *ngIf="projectId" (refreshed)="loadGrantsPage()" [dataSize]="dataSource.totalResult"
<div class="col"> [selection]="selection">
<ng-container *ngIf="!selection.hasValue()"> <ng-template appHasRole [appHasRole]="['project.grant.member.delete:'+projectId, 'project.grant.member.delete']"
<span class="desc">{{'ORG_DETAIL.TABLE.TOTAL' | translate}}</span> actions>
<span class="count">{{dataSource.grantsSubject.value.length}}</span> <button (click)="deleteSelectedGrants()" [disabled]="disabled" mat-icon-button *ngIf="selection.hasValue()"
</ng-container> color="warn">
<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> <i class="las la-trash"></i>
</button> </button>
</ng-template> </ng-template>
<ng-template appHasRole [appHasRole]="['project.grant.member.write:'+projectId,'project.grant.member.write']"> <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 actions>
<a [disabled]="disabled" color="primary" class="rounded-button" color="primary" mat-raised-button
[routerLink]="[ '/projects', projectId, 'grants', 'create']"> [routerLink]="[ '/projects', projectId, 'grants', 'create']">
<mat-icon class="icon">add</mat-icon>{{ 'ACTIONS.NEW' | translate }} <mat-icon class="icon">add</mat-icon>{{ 'ACTIONS.NEW' | translate }}
</a> </a>
</ng-template> </ng-template>
</div>
<div class="table-wrapper"> <div class="table-wrapper">
<div class="spinner-container" *ngIf="dataSource.loading$ | async"> <div class="spinner-container" *ngIf="dataSource.loading$ | async">
<mat-spinner diameter="50"></mat-spinner> <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]"
[length]="dataSource.totalResult" (page)="loadGrantsPage($event.pageIndex, $event.pageSize)">
</mat-paginator>
</div> </div>
<table mat-table multiTemplateDataRows class="full-width-table" aria-label="Elements" [dataSource]="dataSource"> </app-refresh-table>
<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>

View File

@@ -1,30 +1,5 @@
.table-header-row { .rounded-button {
display: flex; border-radius: .5rem;
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 { .table-wrapper {

View File

@@ -52,11 +52,11 @@ export class ProjectGrantsComponent implements OnInit, AfterViewInit {
} }
private loadGrantsPage(): void { public loadGrantsPage(pageIndex?: number, pageSize?: number): void {
this.dataSource.loadGrants( this.dataSource.loadGrants(
this.projectId, this.projectId,
this.paginator.pageIndex, pageIndex ?? this.paginator.pageIndex,
this.paginator.pageSize, pageSize ?? this.paginator.pageSize,
); );
} }

View File

@@ -29,7 +29,7 @@
</div> </div>
<button [ngClass]="{ selected: selection.isSelected(item)}" (click)="selection.toggle(item)" class="edit-button" <button [ngClass]="{ selected: selection.isSelected(item)}" (click)="selection.toggle(item)" class="edit-button"
mat-icon-button> mat-icon-button>
<mat-icon>push_pin_outline</mat-icon> <mat-icon>push_pin</mat-icon>
</button> </button>
</div> </div>
@@ -57,7 +57,7 @@
</div> </div>
<button [ngClass]="{ selected: selection.isSelected(item)}" (click)="selection.toggle(item)" class="edit-button" <button [ngClass]="{ selected: selection.isSelected(item)}" (click)="selection.toggle(item)" class="edit-button"
mat-icon-button> mat-icon-button>
<mat-icon>push_pin_outline</mat-icon> <mat-icon>push_pin</mat-icon>
</button> </button>
</div> </div>

View File

@@ -126,7 +126,7 @@
</div> </div>
</app-card> </app-card>
<app-auth-user-mfa *ngIf="user"></app-auth-user-mfa> <app-auth-user-mfa *ngIf="user" #mfaComponent></app-auth-user-mfa>
</div> </div>
<div *ngIf="user" class="side" metainfo> <div *ngIf="user" class="side" metainfo>

View File

@@ -1,19 +1,37 @@
<app-card title="{{'USER.MFA.TITLE' | translate}}" description="{{'USER.MFA.DESCRIPTION' | translate}}"> <app-card title="{{'USER.MFA.TITLE' | translate}}" description="{{'USER.MFA.DESCRIPTION' | translate}}">
<div class="col"> <app-refresh-table (refreshed)="getOTP()" [dataSize]="dataSource?.data?.length">
<div class="row" *ngFor="let mfa of mfaSubject | async"> <table class="background-style" mat-table [dataSource]="dataSource">
<span>{{'USER.MFA.TYPE.'+ mfa.type | translate}}</span> <ng-container matColumnDef="type">
<i matTooltip="{{'USER.MFA.STATE.'+ mfa.state | translate}}" *ngIf="mfa.state === MFAState.MFASTATE_READY" <th mat-header-cell *matHeaderCellDef> {{ 'USER.MFA.TABLETYPE' | translate }} </th>
class="verified las la-check-circle"></i> <td mat-cell *matCellDef="let mfa"> {{'USER.MFA.TYPE.'+ mfa.type | translate}} </td>
<i matTooltip="{{'USER.MFA.STATE.'+ mfa.state | translate}}" </ng-container>
*ngIf="mfa.state === MFAState.MFASTATE_NOT_READY || mfa.state === MFAState.MFASTATE_REMOVED"
class="primary las la-ban"></i> <ng-container matColumnDef="state">
<button mat-icon-button (click)="deleteMFA(mfa.type)" color="warn" <th mat-header-cell *matHeaderCellDef> {{ 'USER.MFA.TABLESTATE' | translate }} </th>
matTooltip="{{'ACTIONS.DELETE' | translate}}"> <td mat-cell *matCellDef="let mfa"><span class="centered">
<i class="las la-trash"></i> {{'USER.MFA.STATE.'+ mfa.state | translate}}
</button> <i matTooltip="verified" *ngIf="mfa.state === MFAState.MFASTATE_READY"
</div> class="verified las la-check-circle"></i>
<p class="row" *ngIf="error">{{error}}</p> </span>
</div> </td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef> {{ 'USER.MFA.TABLEACTIONS' | translate }} </th>
<td mat-cell *matCellDef="let mfa">
<button matTooltip="{{'ACTIONS.REMOVE' | translate}}" color="warn" mat-icon-button
(click)="deleteMFA(mfa.type)">
<i class="las la-trash"></i>
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr class="data-row" mat-row *matRowDef="let row; columns: displayedColumns;"
[routerLink]="['/users', row.id]">
</tr>
</table>
</app-refresh-table>
<div class="add-row"> <div class="add-row">
<button *ngIf="otpAvailable" (click)="addOTP()" mat-stroked-button color="primary" <button *ngIf="otpAvailable" (click)="addOTP()" mat-stroked-button color="primary"
matTooltip="{{'ACTIONS.NEW' | translate}}"> matTooltip="{{'ACTIONS.NEW' | translate}}">

View File

@@ -1,22 +1,4 @@
.col {
display: flex;
flex-direction: column;
border-bottom: 1px solid #ffffff20;
margin-bottom: 1rem;
.row {
display: flex;
justify-content: space-between;
padding: .5rem 0;
align-items: center;
span {
flex: 1;
}
}
}
.add-row { .add-row {
display: flex; display: flex;
margin: -.5rem; margin: -.5rem;
@@ -24,9 +6,35 @@
button { button {
border-radius: .5rem; border-radius: .5rem;
margin: .5rem; margin: .5rem;
margin-top: 1rem;
mat-icon { mat-icon {
margin-right: .5rem; margin-right: .5rem;
} }
} }
} }
.centered {
display: flex;
align-items: center;
i {
margin-left: 1rem;
color: #5282c1;
}
}
table {
width: 100%;
td, th {
&:first-child {
padding-left: 0;
padding-right: 1rem;
}
&:last-child {
padding-right: 0;
}
}
}

View File

@@ -1,5 +1,7 @@
import { Component, OnDestroy, OnInit } from '@angular/core'; import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { MatSort } from '@angular/material/sort';
import { MatTable, MatTableDataSource } from '@angular/material/table';
import { BehaviorSubject, Observable } from 'rxjs'; import { BehaviorSubject, Observable } from 'rxjs';
import { MfaOtpResponse, MFAState, MfaType, MultiFactor } from 'src/app/proto/generated/auth_pb'; import { MfaOtpResponse, MFAState, MfaType, MultiFactor } from 'src/app/proto/generated/auth_pb';
import { AuthUserService } from 'src/app/services/auth-user.service'; import { AuthUserService } from 'src/app/services/auth-user.service';
@@ -13,28 +15,31 @@ import { DialogOtpComponent } from '../dialog-otp/dialog-otp.component';
styleUrls: ['./auth-user-mfa.component.scss'], styleUrls: ['./auth-user-mfa.component.scss'],
}) })
export class AuthUserMfaComponent implements OnInit, OnDestroy { export class AuthUserMfaComponent implements OnInit, OnDestroy {
public mfaSubject: BehaviorSubject<MultiFactor.AsObject[]> = new BehaviorSubject<MultiFactor.AsObject[]>([]); public displayedColumns: string[] = ['type', 'state', 'actions'];
private loadingSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false); private loadingSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
public loading$: Observable<boolean> = this.loadingSubject.asObservable(); public loading$: Observable<boolean> = this.loadingSubject.asObservable();
@ViewChild(MatTable) public table!: MatTable<MultiFactor.AsObject>;
@ViewChild(MatSort) public sort!: MatSort;
public dataSource!: MatTableDataSource<MultiFactor.AsObject>;
public MfaType: any = MfaType; public MfaType: any = MfaType;
public MFAState: any = MFAState; public MFAState: any = MFAState;
public error: string = ''; public error: string = '';
public otpAvailable: boolean = false; public otpAvailable: boolean = false;
constructor(private userService: AuthUserService, private toast: ToastService, private dialog: MatDialog) { } constructor(private service: AuthUserService, private toast: ToastService, private dialog: MatDialog) { }
public ngOnInit(): void { public ngOnInit(): void {
this.getOTP(); this.getOTP();
} }
public ngOnDestroy(): void { public ngOnDestroy(): void {
this.mfaSubject.complete();
this.loadingSubject.complete(); this.loadingSubject.complete();
} }
public addOTP(): void { public addOTP(): void {
this.userService.AddMfaOTP().then((otpresp) => { this.service.AddMfaOTP().then((otpresp) => {
const otp: MfaOtpResponse.AsObject = otpresp.toObject(); const otp: MfaOtpResponse.AsObject = otpresp.toObject();
const dialogRef = this.dialog.open(DialogOtpComponent, { const dialogRef = this.dialog.open(DialogOtpComponent, {
data: otp.url, data: otp.url,
@@ -43,7 +48,7 @@ export class AuthUserMfaComponent implements OnInit, OnDestroy {
dialogRef.afterClosed().subscribe((code) => { dialogRef.afterClosed().subscribe((code) => {
if (code) { if (code) {
this.userService.VerifyMfaOTP(code).then((res) => { (this.service as AuthUserService).VerifyMfaOTP(code).then(() => {
// TODO: show state // TODO: show state
}); });
} }
@@ -54,28 +59,28 @@ export class AuthUserMfaComponent implements OnInit, OnDestroy {
} }
public getOTP(): void { public getOTP(): void {
this.userService.GetMyMfas().then(mfas => { this.service.GetMyMfas().then(mfas => {
this.mfaSubject.next(mfas.toObject().mfasList); console.log(mfas.toObject().mfasList);
this.dataSource = new MatTableDataSource(mfas.toObject().mfasList);
this.dataSource.sort = this.sort;
const index = mfas.toObject().mfasList.findIndex(mfa => mfa.type === MfaType.MFATYPE_OTP); const index = mfas.toObject().mfasList.findIndex(mfa => mfa.type === MfaType.MFATYPE_OTP);
if (index === -1) { if (index === -1) {
this.otpAvailable = true; this.otpAvailable = true;
} }
}).catch(error => { }).catch(error => {
console.error(error);
this.error = error.message; this.error = error.message;
}); });
} }
public deleteMFA(type: MfaType): void { public deleteMFA(type: MfaType): void {
if (type === MfaType.MFATYPE_OTP) { if (type === MfaType.MFATYPE_OTP) {
this.userService.RemoveMfaOTP().then(() => { this.service.RemoveMfaOTP().then(() => {
this.toast.showInfo('USER.TOAST.OTPREMOVED', true); this.toast.showInfo('USER.TOAST.OTPREMOVED', true);
const index = this.mfaSubject.value.findIndex(mfa => mfa.type === type); const index = this.dataSource.data.findIndex(mfa => mfa.type === type);
if (index > -1) { if (index > -1) {
const newValues = this.mfaSubject.value; this.dataSource.data.splice(index, 1);
newValues.splice(index, 1);
this.mfaSubject.next(newValues);
} }
}).catch(error => { }).catch(error => {

View File

@@ -17,6 +17,7 @@ import { HasRoleModule } from 'src/app/directives/has-role/has-role.module';
import { CardModule } from 'src/app/modules/card/card.module'; import { CardModule } from 'src/app/modules/card/card.module';
import { ChangesModule } from 'src/app/modules/changes/changes.module'; import { ChangesModule } from 'src/app/modules/changes/changes.module';
import { MetaLayoutModule } from 'src/app/modules/meta-layout/meta-layout.module'; import { MetaLayoutModule } from 'src/app/modules/meta-layout/meta-layout.module';
import { RefreshTableModule } from 'src/app/modules/refresh-table/refresh-table.module';
import { SharedModule } from 'src/app/modules/shared/shared.module'; import { SharedModule } from 'src/app/modules/shared/shared.module';
import { UserGrantsModule } from 'src/app/modules/user-grants/user-grants.module'; import { UserGrantsModule } from 'src/app/modules/user-grants/user-grants.module';
import { HasRolePipeModule } from 'src/app/pipes/has-role-pipe.module'; import { HasRolePipeModule } from 'src/app/pipes/has-role-pipe.module';
@@ -68,6 +69,7 @@ import { UserMfaComponent } from './user-detail/user-mfa/user-mfa.component';
MatTableModule, MatTableModule,
MatPaginatorModule, MatPaginatorModule,
SharedModule, SharedModule,
RefreshTableModule,
], ],
}) })
export class UserDetailModule { } export class UserDetailModule { }

View File

@@ -1,12 +1,28 @@
<app-card title="{{'USER.MFA.TITLE' | translate}}" description="{{'USER.MFA.DESCRIPTION' | translate}}" <app-card title="{{'USER.MFA.TITLE' | translate}}" description="{{'USER.MFA.DESCRIPTION' | translate}}">
*ngIf="mfaSubject && (mfaSubject | async)?.length > 0"> <app-refresh-table (refreshed)="getOTP()" [dataSize]="dataSource?.data?.length">
<div class="col"> <table class="background-style" mat-table [dataSource]="dataSource">
<div class="row" *ngFor="let mfa of mfaSubject | async"> <ng-container matColumnDef="type">
<span>{{'USER.MFA.TYPE.'+ mfa.type | translate}}</span> <th mat-header-cell *matHeaderCellDef> {{ 'USER.MFA.TABLETYPE' | translate }} </th>
<span>{{'USER.MFA.STATE.'+ mfa.state | translate}}</span> <td mat-cell *matCellDef="let mfa"> {{'USER.MFA.TYPE.'+ mfa.type | translate}} </td>
</div> </ng-container>
<p class="row" *ngIf="error">{{error}}</p>
</div> <ng-container matColumnDef="state">
<th mat-header-cell *matHeaderCellDef> {{ 'USER.MFA.TABLESTATE' | translate }} </th>
<td mat-cell *matCellDef="let mfa">
<span class="centered">
{{'USER.MFA.STATE.'+ mfa.state | translate}}
<i matTooltip="verified" *ngIf="mfa.state === MFAState.MFASTATE_READY"
class="verified las la-check-circle"></i>
</span>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr class="data-row" mat-row *matRowDef="let row; columns: displayedColumns;"
[routerLink]="['/users', row.id]">
</tr>
</table>
</app-refresh-table>
<div class="table-wrapper"> <div class="table-wrapper">
<div class="spinner-container" *ngIf="loading$ | async"> <div class="spinner-container" *ngIf="loading$ | async">
<mat-spinner diameter="50"></mat-spinner> <mat-spinner diameter="50"></mat-spinner>

View File

@@ -16,3 +16,28 @@
} }
} }
} }
.centered {
display: flex;
align-items: center;
i {
margin-left: 1rem;
color: #5282c1;
}
}
table {
width: 100%;
td, th {
&:first-child {
padding-left: 0;
padding-right: 1rem;
}
&:last-child {
padding-right: 0;
}
}
}

View File

@@ -1,4 +1,6 @@
import { Component, Input, OnDestroy, OnInit } from '@angular/core'; import { Component, Input, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { MatSort } from '@angular/material/sort';
import { MatTable, MatTableDataSource } from '@angular/material/table';
import { BehaviorSubject, Observable } from 'rxjs'; import { BehaviorSubject, Observable } from 'rxjs';
import { MFAState, MfaType, MultiFactor, UserView } from 'src/app/proto/generated/management_pb'; import { MFAState, MfaType, MultiFactor, UserView } 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';
@@ -15,11 +17,16 @@ export interface MFAItem {
styleUrls: ['./user-mfa.component.scss'], styleUrls: ['./user-mfa.component.scss'],
}) })
export class UserMfaComponent implements OnInit, OnDestroy { export class UserMfaComponent implements OnInit, OnDestroy {
public displayedColumns: string[] = ['type', 'state'];
@Input() private user!: UserView.AsObject; @Input() private user!: UserView.AsObject;
public mfaSubject: BehaviorSubject<MultiFactor.AsObject[]> = new BehaviorSubject<MultiFactor.AsObject[]>([]); public mfaSubject: BehaviorSubject<MultiFactor.AsObject[]> = new BehaviorSubject<MultiFactor.AsObject[]>([]);
private loadingSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false); private loadingSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
public loading$: Observable<boolean> = this.loadingSubject.asObservable(); public loading$: Observable<boolean> = this.loadingSubject.asObservable();
@ViewChild(MatTable) public table!: MatTable<MultiFactor.AsObject>;
@ViewChild(MatSort) public sort!: MatSort;
public dataSource!: MatTableDataSource<MultiFactor.AsObject>;
public MfaType: any = MfaType; public MfaType: any = MfaType;
public MFAState: any = MFAState; public MFAState: any = MFAState;
@@ -37,10 +44,10 @@ export class UserMfaComponent implements OnInit, OnDestroy {
public getOTP(): void { public getOTP(): void {
this.mgmtUserService.getUserMfas(this.user.id).then(mfas => { this.mgmtUserService.getUserMfas(this.user.id).then(mfas => {
this.mfaSubject.next(mfas.toObject().mfasList); console.log(mfas.toObject().mfasList);
this.error = ''; this.dataSource = new MatTableDataSource(mfas.toObject().mfasList);
this.dataSource.sort = this.sort;
}).catch(error => { }).catch(error => {
console.error(error);
this.error = error.message; this.error = error.message;
}); });
} }

View File

@@ -2,19 +2,8 @@
<h1>{{ 'USER.PAGES.LIST' | translate }}</h1> <h1>{{ 'USER.PAGES.LIST' | translate }}</h1>
<p class="sub">{{ 'USER.PAGES.DESCRIPTION' | translate }}</p> <p class="sub">{{ 'USER.PAGES.DESCRIPTION' | translate }}</p>
<div class="table-header-row"> <app-refresh-table (refreshed)="refreshPage()" [dataSize]="dataSource.data.length">
<div class="col"> <ng-template appHasRole [appHasRole]="['user.write']" actions>
<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>
<ng-template appHasRole [appHasRole]="['user.write']">
<button (click)="deactivateSelectedUsers()" matTooltip="{{'ORG_DETAIL.TABLE.DEACTIVATE' | translate}}" <button (click)="deactivateSelectedUsers()" matTooltip="{{'ORG_DETAIL.TABLE.DEACTIVATE' | translate}}"
class="icon-button" mat-icon-button *ngIf="selection.hasValue()"> class="icon-button" mat-icon-button *ngIf="selection.hasValue()">
<mat-icon svgIcon="mdi_account_cancel"></mat-icon> <mat-icon svgIcon="mdi_account_cancel"></mat-icon>
@@ -27,60 +16,61 @@
<mat-icon class="icon">add</mat-icon>{{ 'ACTIONS.NEW' | translate }} <mat-icon class="icon">add</mat-icon>{{ 'ACTIONS.NEW' | translate }}
</a> </a>
</ng-template> </ng-template>
</div>
<div class="table-wrapper"> <div class="table-wrapper">
<div class="spinner-container" *ngIf="loading$ | async"> <div class="spinner-container" *ngIf="loading$ | async">
<mat-spinner diameter="50"></mat-spinner> <mat-spinner diameter="50"></mat-spinner>
</div>
<table class="background-style" mat-table [dataSource]="dataSource">
<ng-container matColumnDef="select">
<th mat-header-cell *matHeaderCellDef>
<mat-checkbox color="primary" (change)="$event ? masterToggle() : null"
[checked]="selection.hasValue() && isAllSelected()"
[indeterminate]="selection.hasValue() && !isAllSelected()">
</mat-checkbox>
</th>
<td mat-cell *matCellDef="let user">
<mat-checkbox color="primary" (click)="$event.stopPropagation()"
(change)="$event ? selection.toggle(user) : null" [checked]="selection.isSelected(user)">
<app-avatar *ngIf="user && user.displayName" class="avatar" [name]="user.displayName"
[size]="32">
</app-avatar>
</mat-checkbox>
</td>
</ng-container>
<ng-container matColumnDef="firstname">
<th mat-header-cell *matHeaderCellDef> {{ 'USER.PROFILE.FIRSTNAME' | translate }} </th>
<td mat-cell *matCellDef="let user"> {{user.firstName}} </td>
</ng-container>
<ng-container matColumnDef="lastname">
<th mat-header-cell *matHeaderCellDef> {{ 'USER.PROFILE.LASTNAME' | translate }} </th>
<td mat-cell *matCellDef="let user"> {{user.lastName}} </td>
</ng-container>
<ng-container matColumnDef="username">
<th mat-header-cell *matHeaderCellDef> {{ 'USER.PROFILE.USERNAME' | translate }} </th>
<td mat-cell *matCellDef="let user"> {{user.userName}} </td>
</ng-container>
<ng-container matColumnDef="email">
<th mat-header-cell *matHeaderCellDef> {{ 'USER.EMAIL' | translate }} </th>
<td mat-cell *matCellDef="let user"> {{user.email}} </td>
</ng-container>
<ng-container matColumnDef="state">
<th mat-header-cell *matHeaderCellDef> {{ 'USER.DATA.STATE' | translate }} </th>
<td mat-cell *matCellDef="let user"> {{ 'USER.DATA.STATE'+user.state | translate }} </td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr class="data-row" mat-row *matRowDef="let row; columns: displayedColumns;"
[routerLink]="['/users', row.id]">
</tr>
</table>
<mat-paginator #paginator class="background-style" [length]="userResult?.totalResult || 0" [pageSize]="10"
[pageSizeOptions]="[5, 10, 20]" (page)="changePage($event)"></mat-paginator>
</div> </div>
<table class="background-style" mat-table [dataSource]="dataSource"> </app-refresh-table>
<ng-container matColumnDef="select">
<th mat-header-cell *matHeaderCellDef>
<mat-checkbox color="primary" (change)="$event ? masterToggle() : null"
[checked]="selection.hasValue() && isAllSelected()"
[indeterminate]="selection.hasValue() && !isAllSelected()">
</mat-checkbox>
</th>
<td mat-cell *matCellDef="let user">
<mat-checkbox color="primary" (click)="$event.stopPropagation()"
(change)="$event ? selection.toggle(user) : null" [checked]="selection.isSelected(user)">
<app-avatar *ngIf="user && user.displayName" class="avatar" [name]="user.displayName"
[size]="32">
</app-avatar>
</mat-checkbox>
</td>
</ng-container>
<ng-container matColumnDef="firstname">
<th mat-header-cell *matHeaderCellDef> {{ 'USER.PROFILE.FIRSTNAME' | translate }} </th>
<td mat-cell *matCellDef="let user"> {{user.firstName}} </td>
</ng-container>
<ng-container matColumnDef="lastname">
<th mat-header-cell *matHeaderCellDef> {{ 'USER.PROFILE.LASTNAME' | translate }} </th>
<td mat-cell *matCellDef="let user"> {{user.lastName}} </td>
</ng-container>
<ng-container matColumnDef="username">
<th mat-header-cell *matHeaderCellDef> {{ 'USER.PROFILE.USERNAME' | translate }} </th>
<td mat-cell *matCellDef="let user"> {{user.userName}} </td>
</ng-container>
<ng-container matColumnDef="email">
<th mat-header-cell *matHeaderCellDef> {{ 'USER.EMAIL' | translate }} </th>
<td mat-cell *matCellDef="let user"> {{user.email}} </td>
</ng-container>
<ng-container matColumnDef="state">
<th mat-header-cell *matHeaderCellDef> {{ 'USER.DATA.STATE' | translate }} </th>
<td mat-cell *matCellDef="let user"> {{ 'USER.DATA.STATE'+user.state | translate }} </td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr class="data-row" mat-row *matRowDef="let row; columns: displayedColumns;"
[routerLink]="['/users', row.id]">
</tr>
</table>
<mat-paginator class="background-style" [length]="userResult?.totalResult || 0" [pageSize]="10"
[pageSizeOptions]="[5, 10, 20]" (page)="changePage($event)"></mat-paginator>
</div>
</div> </div>

View File

@@ -7,34 +7,8 @@ h1 {
margin-bottom: 2rem; margin-bottom: 2rem;
} }
.table-header-row { .add-button {
display: flex; border-radius: .5rem;
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 { .table-wrapper {

View File

@@ -1,6 +1,6 @@
import { SelectionModel } from '@angular/cdk/collections'; import { SelectionModel } from '@angular/cdk/collections';
import { Component, EventEmitter, OnDestroy, Output } from '@angular/core'; import { Component, EventEmitter, OnDestroy, Output, ViewChild } from '@angular/core';
import { PageEvent } from '@angular/material/paginator'; import { MatPaginator, PageEvent } from '@angular/material/paginator';
import { MatTableDataSource } from '@angular/material/table'; import { MatTableDataSource } from '@angular/material/table';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
@@ -15,6 +15,7 @@ import { ToastService } from 'src/app/services/toast.service';
styleUrls: ['./user-list.component.scss'], styleUrls: ['./user-list.component.scss'],
}) })
export class UserListComponent implements OnDestroy { export class UserListComponent implements OnDestroy {
@ViewChild(MatPaginator) public paginator!: MatPaginator;
public dataSource: MatTableDataSource<User.AsObject> = new MatTableDataSource<User.AsObject>(); public dataSource: MatTableDataSource<User.AsObject> = new MatTableDataSource<User.AsObject>();
public userResult!: UserSearchResponse.AsObject; public userResult!: UserSearchResponse.AsObject;
private loadingSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false); private loadingSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
@@ -83,4 +84,8 @@ export class UserListComponent implements OnDestroy {
this.loadingSubject.next(false); this.loadingSubject.next(false);
}); });
} }
public refreshPage(): void {
this.getData(this.paginator.pageSize, this.paginator.pageIndex * this.paginator.pageSize);
}
} }

View File

@@ -13,13 +13,13 @@ import { TranslateModule } from '@ngx-translate/core';
import { HasRoleModule } from 'src/app/directives/has-role/has-role.module'; import { HasRoleModule } from 'src/app/directives/has-role/has-role.module';
import { AvatarModule } from 'src/app/modules/avatar/avatar.module'; import { AvatarModule } from 'src/app/modules/avatar/avatar.module';
import { CardModule } from 'src/app/modules/card/card.module'; import { CardModule } from 'src/app/modules/card/card.module';
import { RefreshTableModule } from 'src/app/modules/refresh-table/refresh-table.module';
import { SharedModule } from 'src/app/modules/shared/shared.module'; import { SharedModule } from 'src/app/modules/shared/shared.module';
import { UserListRoutingModule } from './user-list-routing.module'; import { UserListRoutingModule } from './user-list-routing.module';
import { UserListComponent } from './user-list.component'; import { UserListComponent } from './user-list.component';
@NgModule({ @NgModule({
declarations: [ declarations: [
UserListComponent, UserListComponent,
@@ -41,6 +41,7 @@ import { UserListComponent } from './user-list.component';
MatTooltipModule, MatTooltipModule,
TranslateModule, TranslateModule,
SharedModule, SharedModule,
RefreshTableModule,
], ],
exports: [ exports: [
UserListComponent, UserListComponent,

View File

@@ -1,6 +1,6 @@
import { PlatformLocation } from '@angular/common';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { PlatformLocation } from '@angular/common';
import { AdminServicePromiseClient } from '../proto/generated/admin_grpc_web_pb'; import { AdminServicePromiseClient } from '../proto/generated/admin_grpc_web_pb';
import { AuthServicePromiseClient } from '../proto/generated/auth_grpc_web_pb'; import { AuthServicePromiseClient } from '../proto/generated/auth_grpc_web_pb';
@@ -28,7 +28,10 @@ export class GrpcService {
public async loadAppEnvironment(): Promise<any> { public async loadAppEnvironment(): Promise<any> {
return this.http.get('./assets/environment.json') return this.http.get('./assets/environment.json')
.toPromise().then((data: any) => { .toPromise().then((data: any) => {
console.log('init grpc');
if (data && data.authServiceUrl && data.mgmtServiceUrl && data.issuer) { if (data && data.authServiceUrl && data.mgmtServiceUrl && data.issuer) {
console.log('init grpc promiseclients');
this.auth = new AuthServicePromiseClient(data.authServiceUrl); this.auth = new AuthServicePromiseClient(data.authServiceUrl);
this.mgmt = new ManagementServicePromiseClient(data.mgmtServiceUrl); this.mgmt = new ManagementServicePromiseClient(data.mgmtServiceUrl);
this.admin = new AdminServicePromiseClient(data.adminServiceUrl); this.admin = new AdminServicePromiseClient(data.adminServiceUrl);

View File

@@ -217,23 +217,6 @@ export class MgmtUserService {
); );
} }
// public async CreateUserGrant(
// projectId: string,
// userId: string,
// roleNamesList: string[],
// ): Promise<UserGrant> {
// const req = new UserGrantCreate();
// req.setProjectId(projectId);
// req.setUserId(userId);
// req.setRoleKeysList(roleNamesList);
// return await this.request(
// c => c.createUserGrant,
// req,
// f => f,
// );
// }
public async CreateProjectUserGrant( public async CreateProjectUserGrant(
projectId: string, projectId: string,
userId: string, userId: string,

View File

@@ -50,7 +50,8 @@
"FINISH":"Abschliessen", "FINISH":"Abschliessen",
"CHANGE":"Ändern", "CHANGE":"Ändern",
"REACTIVATE":"Aktivieren", "REACTIVATE":"Aktivieren",
"DEACTIVATE":"Deaktivieren" "DEACTIVATE":"Deaktivieren",
"REFRESH":"Aktualisieren"
}, },
"ERRORS": { "ERRORS": {
"REQUIRED": "Bitte fülle alle benötigten Felder aus!" "REQUIRED": "Bitte fülle alle benötigten Felder aus!"
@@ -74,6 +75,9 @@
"DEACTIVATE":"Deaktivieren" "DEACTIVATE":"Deaktivieren"
}, },
"MFA": { "MFA": {
"TABLETYPE":"Typ",
"TABLESTATE":"Status",
"TABLEACTIONS":"Aktionen",
"TITLE": "Multifaktor-Authentifizierung", "TITLE": "Multifaktor-Authentifizierung",
"DESCRIPTION": "Füge einen zusätzlichen Faktor hinzu, um deinen Account optimal zu schützen.", "DESCRIPTION": "Füge einen zusätzlichen Faktor hinzu, um deinen Account optimal zu schützen.",
"MANAGE_DESCRIPTION": "Verwalte die Multifaktor Merkmale deiner Benutzer.", "MANAGE_DESCRIPTION": "Verwalte die Multifaktor Merkmale deiner Benutzer.",

View File

@@ -49,8 +49,9 @@
"VERIFY":"Verify", "VERIFY":"Verify",
"FINISH":"Finish", "FINISH":"Finish",
"CHANGE":"Change", "CHANGE":"Change",
"REACTIVATE":"Aktivieren", "REACTIVATE":"Reactivate",
"DEACTIVATE":"Deaktivieren" "DEACTIVATE":"Deactivate",
"REFRESH":"Refresh"
}, },
"ERRORS": { "ERRORS": {
"REQUIRED": "Some required fields are missing!" "REQUIRED": "Some required fields are missing!"
@@ -74,6 +75,9 @@
"DEACTIVATE":"Deactivate" "DEACTIVATE":"Deactivate"
}, },
"MFA": { "MFA": {
"TABLETYPE":"Type",
"TABLESTATE":"Status",
"TABLEACTIONS":"Actions",
"TITLE": "Multifactor Authentication", "TITLE": "Multifactor Authentication",
"DESCRIPTION": "Add a second factor to ensure optimal security for your account.", "DESCRIPTION": "Add a second factor to ensure optimal security for your account.",
"MANAGE_DESCRIPTION": "Manage the second factor methods of your users.", "MANAGE_DESCRIPTION": "Manage the second factor methods of your users.",

View File

@@ -9,7 +9,7 @@
<link rel="icon" type="image/x-icon" href="favicon.ico"> <link rel="icon" type="image/x-icon" href="favicon.ico">
<link href="https://fonts.googleapis.com/css?family=Manjari|Roboto+Slab&amp;display=swap" rel="stylesheet" /> <link href="https://fonts.googleapis.com/css?family=Manjari|Roboto+Slab&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/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|Material+Icons+Outlined" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Lato&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Lato&display=swap" rel="stylesheet">
<link 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"> href="https://maxst.icons8.com/vue-static/landings/line-awesome/line-awesome/1.3.0/css/line-awesome.min.css">

View File

@@ -91,16 +91,49 @@ $caos-light-brand: (
A700: white, A700: white,
) )
); );
$caos-accent-color: (
50: #ebf4f2,
100: #cce3de,
200: #abd1c9,
300: #89bfb3,
400: #6fb1a2,
500: #56a392,
600: #4f9b8a,
700: #45917f,
800: #3c8875,
900: #2b7763,
A100: #beffed,
A200: #8bffde,
A400: #58ffd0,
A700: #3effc9,
contrast: (
50: $black-87-opacity,
100: $black-87-opacity,
200: $black-87-opacity,
300: $black-87-opacity,
400: $black-87-opacity,
500: white,
600: white,
700: white,
800: white,
900: white,
A100: $black-87-opacity,
A200: $black-87-opacity,
A400: $black-87-opacity,
A700: white,
)
);
// Define the palettes for your theme using the Material Design palettes available in palette.scss // Define the palettes for your theme using the Material Design palettes available in palette.scss
// (imported above). For each palette, you can optionally specify a default, lighter, and darker // (imported above). For each palette, you can optionally specify a default, lighter, and darker
// hue. Available color palettes: https://material.io/design/color/ // hue. Available color palettes: https://material.io/design/color/
$light-primary: mat-palette($caos-light-brand); $light-primary: mat-palette($caos-light-brand);
$light-accent: mat-palette($mat-pink); $light-accent:mat-palette($caos-accent-color);
$light-warn: mat-palette($mat-red); $light-warn: mat-palette($mat-red);
$dark-primary: mat-palette($caos-dark-brand); $dark-primary: mat-palette($caos-dark-brand);
$dark-accent: mat-palette($mat-pink, 500, 700, A700); $dark-accent: mat-palette($mat-pink);
$dark-warn: mat-palette($mat-red); $dark-warn: mat-palette($mat-red);
$light-theme: mat-light-theme($light-primary, $light-accent, $light-warn); $light-theme: mat-light-theme($light-primary, $light-accent, $light-warn);
@@ -140,8 +173,9 @@ $custom-typography: mat-typography-config(
} }
::-webkit-scrollbar-thumb { ::-webkit-scrollbar-thumb {
background-color: #5282c1; background-color: #737C8850;
border-radius: 8px; border-radius: 8px;
cursor: pointer;
} }
} }
@@ -172,8 +206,9 @@ $custom-typography: mat-typography-config(
} }
::-webkit-scrollbar-thumb { ::-webkit-scrollbar-thumb {
background-color: #5282c1; background-color: #737C8870;
border-radius: 8px; border-radius: 8px;
cursor: pointer;
} }
} }

View File

@@ -48,9 +48,9 @@
} }
.root-header { .root-header {
@include mat-elevation(3); box-shadow: 0px 2px 1px -1px rgba(0, 0, 0, 0.1), 0px 1px 1px 0px rgba(0, 0, 0, 0.1), 0px 1px 3px 0px rgba(0, 0, 0, 0.1);
background: $primary-dark !important; background-color: $primary-dark !important;
transition: background .5s ease-in-out; transition: background-color .5s ease-in-out;
} }
.admin-line { .admin-line {