fix(console): hide granted project navigation if none, cache zitadel permissions, emit refresh on org change, cleanup contributors, styling (#511)

* fix iam member model

* fix org member model

* fix auth user loading

* copytoclipboard directive

* directive logs, load bar on init, create user

* typo

* welcome section, contributor spinner

* fix home link

* fix stepper flow

* show dialog on invalid token

* fix app table refresh, pin icons light theme

* cleanup contributor

* inherit parent color, animations

* use localized date pipe everywhere

* cmp styles refactor, dont show granted p if none

* fix navitem desc, fixed header

* change permissions, caching

* roles on org emit, use prom instead of hot obs

* dont calc 100vh
This commit is contained in:
Max Peintner
2020-07-28 09:09:18 +02:00
committed by GitHub
parent 2d8f934a07
commit 531060ab67
37 changed files with 442 additions and 296 deletions

View File

@@ -0,0 +1,153 @@
import {
animate,
animateChild,
AnimationTriggerMetadata,
group,
query,
stagger,
style,
transition,
trigger,
} from '@angular/animations';
export const toolbarAnimation: AnimationTriggerMetadata =
trigger('toolbar', [
transition(':enter', [
style({
transform: 'translateY(-100%)',
opacity: 0,
}),
animate(
'.2s ease-out',
style({
transform: 'translateY(0%)',
opacity: 1,
}),
),
]),
]);
export const accountCard: AnimationTriggerMetadata = trigger('accounts', [
transition(':enter', [
style({
transform: 'scale(.9) translateY(-10%)',
height: '200px',
opacity: 0,
}),
animate(
'.1s ease-out',
style({
transform: 'scale(1) translateY(0%)',
height: '*',
opacity: 1,
}),
),
]),
]);
export const navAnimations: Array<AnimationTriggerMetadata> = [
trigger('navAnimation', [
transition('* => *', [
query('@navitem', stagger('50ms', animateChild()), { optional: true }),
]),
]),
trigger('navitem', [
transition(':enter', [
style({
opacity: 0,
}),
animate(
'.0s',
style({
opacity: 1,
}),
),
]),
transition(':leave', [
style({
opacity: 1,
}),
animate(
'.0s',
style({
opacity: 0,
}),
),
]),
]),
];
export const routeAnimations: AnimationTriggerMetadata = trigger('routeAnimations', [
transition('HomePage => AddPage', [
style({ transform: 'translateX(100%)' }),
animate('250ms ease-in-out', style({ transform: 'translateX(0%)' })),
]),
transition('AddPage => HomePage', [animate('250ms', style({ transform: 'translateX(100%)' }))]),
transition('HomePage => DetailPage', [
query(':enter, :leave', style({ position: 'absolute', left: 0, right: 0 }), {
optional: true,
}),
group([
query(
':enter',
[
style({
transform: 'translateX(20%)',
opacity: 0.5,
}),
animate(
'.35s ease-in',
style({
transform: 'translateX(0%)',
opacity: 1,
}),
),
],
{
optional: true,
},
),
query(
':leave',
[style({ opacity: 1, width: '100%' }), animate('.35s ease-out', style({ opacity: 0 }))],
{
optional: true,
},
),
]),
]),
transition('DetailPage => HomePage', [
query(':enter, :leave', style({ position: 'absolute', left: 0, right: 0 }), {
optional: true,
}),
group([
query(
':enter',
[
style({
opacity: 0,
}),
animate(
'.35s ease-out',
style({
opacity: 1,
}),
),
],
{
optional: true,
},
),
query(
':leave',
[
style({ width: '100%', transform: 'translateX(0%)' }),
animate('.35s ease-in', style({ transform: 'translateX(30%)', opacity: 0 })),
],
{
optional: true,
},
),
]),
]),
]);

View File

@@ -1,6 +1,6 @@
<ng-container *ngIf="(authService.user | async) || {} as user"> <ng-container *ngIf="(authService.user | async) || {} as user">
<ng-container *ngIf="((['iam.read','iam.write'] | hasRole)) as iamuser$"> <ng-container *ngIf="((['iam.read','iam.write'] | hasRole)) as iamuser$">
<mat-toolbar class="root-header"> <mat-toolbar @toolbar class="root-header">
<button aria-label="Toggle sidenav" mat-icon-button (click)="drawer.toggle()"> <button aria-label="Toggle sidenav" mat-icon-button (click)="drawer.toggle()">
<mat-icon aria-label="Side nav toggle icon">menu</mat-icon> <mat-icon aria-label="Side nav toggle icon">menu</mat-icon>
</button> </button>
@@ -53,7 +53,7 @@
<div class="side-column"> <div class="side-column">
<div class="list"> <div class="list">
<ng-container *ngIf="authService.authenticationChanged | async"> <ng-container *ngIf="authService.authenticationChanged | async">
<a class="nav-item" [routerLinkActive]="['active']" <a @navitem class="nav-item" [routerLinkActive]="['active']"
[routerLinkActiveOptions]="{ exact: true }" [routerLink]="['/users/me']"> [routerLinkActiveOptions]="{ exact: true }" [routerLink]="['/users/me']">
<i class="icon las la-user-circle"></i> <i class="icon las la-user-circle"></i>
<span class="label">{{ 'MENU.PERSONAL_INFO' | translate }}</span> <span class="label">{{ 'MENU.PERSONAL_INFO' | translate }}</span>
@@ -64,52 +64,65 @@
<div class="divider"> <div class="divider">
<div class="line"></div> <div class="line"></div>
</div> </div>
<a class="nav-item" [routerLinkActive]="['active']" [routerLink]="[ '/iam']"> <a @navitem class="nav-item" [routerLinkActive]="['active']" [routerLink]="[ '/iam']">
<i class="icon las la-gem"></i> <i class="icon las la-gem"></i>
<span class="label">{{'MENU.IAM' | translate}}</span> <span class="label">{{'MENU.IAM' | translate}}</span>
</a> </a>
</ng-container> </ng-container>
<ng-template appHasRole [appHasRole]="['org.read']"> <div *ngIf="org" [@navAnimation]="org">
<a class="nav-item" [routerLinkActive]="['active']" [routerLink]="[ '/org']"> <ng-template appHasRole [appHasRole]="['org.read']">
<i class="icon las la-archway"></i> <a @navitem class="nav-item" [routerLinkActive]="['active']" [routerLink]="[ '/org']">
<span class="label">{{org?.name ? org.name : 'MENU.ORGANIZATION' | translate}}</span> <i class="icon las la-archway"></i>
</a> <span
</ng-template> class="label">{{org?.name ? org.name : 'MENU.ORGANIZATION' | translate}}</span>
</a>
</ng-template>
<ng-template appHasRole [appHasRole]="['project.read']"> <ng-template appHasRole [appHasRole]="['project.read']">
<div class="divider"> <div @navitem class="divider">
<div class="line"></div> <div class="line"></div>
<span>{{'MENU.PROJECTSSECTION' | translate}}</span> <span>{{'MENU.PROJECTSSECTION' | translate}}</span>
<div class="line"></div> <div class="line"></div>
</div> </div>
<a class="nav-item" [routerLinkActive]="['active']" [routerLink]="[ '/projects']"> <a @navitem class="nav-item" [routerLinkActive]="['active']"
<i class="icon las la-layer-group"></i> [routerLink]="[ '/projects']">
<span class="label">{{org?.name ? org.name : 'MENU.ORGANIZATION' | translate}} <i class="icon las la-layer-group"></i>
{{ 'MENU.PROJECT' | translate }}</span>
</a>
<a class="nav-item" [routerLinkActive]="['active']" [routerLink]="[ '/granted-projects']"> <div class="c_label">
<i class="icon las la-layer-group"></i> <span>{{org?.name ? org.name : 'MENU.ORGANIZATION' | translate}}
<span class="label">{{ 'MENU.GRANTEDPROJECT' | translate }}</span> {{'MENU.PROJECT' | translate}} </span>
</a> <span *ngIf="ownedProjectsCount as ownedPCount"
</ng-template> class="count">{{ownedPCount}}</span>
</div>
</a>
<ng-template appHasRole [appHasRole]="['user.read']"> <a @navitem *ngIf="grantedProjectsCount as grantPCount" class="nav-item"
<div class="divider"> [routerLinkActive]="['active']" [routerLink]="[ '/granted-projects']">
<div class="line"></div> <i class="icon las la-layer-group"></i>
<span class="label"> <div class="c_label">
{{ 'MENU.USERSECTION' | translate }}</span> <span>{{ 'MENU.GRANTEDPROJECT' | translate }}</span>
<div class="line"></div> <span class="count">{{grantPCount}}</span>
</div> </div>
</a>
</ng-template>
<a class="nav-item" [routerLinkActive]="['active']" [routerLink]="[ '/users/all']" <ng-template appHasRole [appHasRole]="['user.read']">
[routerLinkActiveOptions]="{ exact: true }"> <div @navitem class="divider">
<i class="icon las la-users"></i> <div class="line"></div>
<span class="label">{{ 'MENU.USER' | translate }}</span> <span class="label">
</a> {{ 'MENU.USERSECTION' | translate }}</span>
</ng-template> <div class="line"></div>
</div>
<a @navitem class="nav-item" [routerLinkActive]="['active']"
[routerLink]="[ '/users/all']" [routerLinkActiveOptions]="{ exact: true }">
<i class="icon las la-users"></i>
<span class="label">{{ 'MENU.USER' | translate }}</span>
</a>
</ng-template>
</div>
<span class="fill-space"></span> <span class="fill-space"></span>
</div> </div>
@@ -117,7 +130,7 @@
</div> </div>
</mat-drawer> </mat-drawer>
<mat-drawer-content class="content"> <mat-drawer-content class="content">
<div *ngIf="iamuser$ | async" class="admin-line" matTooltip="IAM Administrator"> <div @toolbar *ngIf="iamuser$ | async" class="admin-line" matTooltip="IAM Administrator">
<span>{{'MENU.IAMADMIN' | translate}}</span> <span>{{'MENU.IAMADMIN' | translate}}</span>
</div> </div>
<div class="router" [@routeAnimations]="prepareRoute(outlet)"> <div class="router" [@routeAnimations]="prepareRoute(outlet)">

View File

@@ -1,11 +1,14 @@
.root-header { .root-header {
position: relative; position: fixed;
z-index: 100; z-index: 100;
display: flex; display: flex;
height: 60px; height: 60px;
align-items: center; align-items: center;
padding: 0 1rem; padding: 0 1rem;
top: 0;
left: 0;
right: 0;
.logo { .logo {
height: 40px; height: 40px;
@@ -74,15 +77,16 @@
.main-container { .main-container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: calc(100vh - 60px); height: 100vh;
width: 100%; width: 100%;
padding-top: 60px;
.sidenav { .sidenav {
width: 300px; width: 300px;
border-right: none; border-right: none;
.side-column { .side-column {
height: calc(100vh - 70px); padding-top: 60px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: stretch; align-items: stretch;
@@ -120,6 +124,17 @@
font-weight: 500; font-weight: 500;
font-size: .9rem; font-size: .9rem;
} }
.c_label {
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
.count {
font-size: 12px;
}
}
&:hover { &:hover {
background-color: #00000010; background-color: #00000010;

View File

@@ -1,4 +1,3 @@
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 { ViewportScroller } from '@angular/common'; import { ViewportScroller } from '@angular/common';
@@ -11,9 +10,11 @@ import { TranslateService } from '@ngx-translate/core';
import { Observable, of, Subscription } from 'rxjs'; import { Observable, of, Subscription } from 'rxjs';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
import { accountCard, navAnimations, routeAnimations, toolbarAnimation } from './animations';
import { Org, UserProfileView } from './proto/generated/auth_pb'; import { Org, UserProfileView } from './proto/generated/auth_pb';
import { AuthUserService } from './services/auth-user.service'; import { AuthUserService } from './services/auth-user.service';
import { AuthService } from './services/auth.service'; import { AuthService } from './services/auth.service';
import { ProjectService } from './services/project.service';
import { ThemeService } from './services/theme.service'; import { ThemeService } from './services/theme.service';
import { ToastService } from './services/toast.service'; import { ToastService } from './services/toast.service';
import { UpdateService } from './services/update.service'; import { UpdateService } from './services/update.service';
@@ -23,97 +24,10 @@ import { UpdateService } from './services/update.service';
templateUrl: './app.component.html', templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'], styleUrls: ['./app.component.scss'],
animations: [ animations: [
trigger('accounts', [ toolbarAnimation,
transition(':enter', [ ...navAnimations,
style({ accountCard,
transform: 'scale(.9) translateY(-10%)', routeAnimations,
height: '200px',
opacity: 0,
}),
animate(
'.1s ease-out',
style({
transform: 'scale(1) translateY(0%)',
height: '*',
opacity: 1,
}),
),
]),
]),
trigger('routeAnimations', [
transition('HomePage => AddPage', [
style({ transform: 'translateX(100%)' }),
animate('250ms ease-in-out', style({ transform: 'translateX(0%)' })),
]),
transition('AddPage => HomePage', [animate('250ms', style({ transform: 'translateX(100%)' }))]),
transition('HomePage => DetailPage', [
query(':enter, :leave', style({ position: 'absolute', left: 0, right: 0 }), {
optional: true,
}),
group([
query(
':enter',
[
style({
transform: 'translateX(20%)',
opacity: 0.5,
}),
animate(
'.35s ease-in',
style({
transform: 'translateX(0%)',
opacity: 1,
}),
),
],
{
optional: true,
},
),
query(
':leave',
[style({ opacity: 1, width: '100%' }), animate('.35s ease-out', style({ opacity: 0 }))],
{
optional: true,
},
),
]),
]),
transition('DetailPage => HomePage', [
query(':enter, :leave', style({ position: 'absolute', left: 0, right: 0 }), {
optional: true,
}),
group([
query(
':enter',
[
style({
opacity: 0,
}),
animate(
'.35s ease-out',
style({
opacity: 1,
}),
),
],
{
optional: true,
},
),
query(
':leave',
[
style({ width: '100%', transform: 'translateX(0%)' }),
animate('.35s ease-in', style({ transform: 'translateX(30%)', opacity: 0 })),
],
{
optional: true,
},
),
]),
]),
]),
], ],
}) })
export class AppComponent implements OnDestroy { export class AppComponent implements OnDestroy {
@@ -135,6 +49,10 @@ export class AppComponent implements OnDestroy {
public orgLoading: boolean = false; public orgLoading: boolean = false;
public showProjectSection: boolean = false; public showProjectSection: boolean = false;
public grantedProjectsCount: number = 0;
public ownedProjectsCount: number = 0;
private authSub: Subscription = new Subscription(); private authSub: Subscription = new Subscription();
private orgSub: Subscription = new Subscription(); private orgSub: Subscription = new Subscription();
@@ -147,6 +65,7 @@ export class AppComponent implements OnDestroy {
public overlayContainer: OverlayContainer, public overlayContainer: OverlayContainer,
private themeService: ThemeService, private themeService: ThemeService,
public userService: AuthUserService, public userService: AuthUserService,
private projectService: ProjectService,
public matIconRegistry: MatIconRegistry, public matIconRegistry: MatIconRegistry,
public domSanitizer: DomSanitizer, public domSanitizer: DomSanitizer,
private toast: ToastService, private toast: ToastService,
@@ -218,9 +137,12 @@ export class AppComponent implements OnDestroy {
'mdi_pin', 'mdi_pin',
this.domSanitizer.bypassSecurityTrustResourceUrl('assets/mdi/pin.svg'), this.domSanitizer.bypassSecurityTrustResourceUrl('assets/mdi/pin.svg'),
); );
this.getProjectCount();
this.orgSub = this.authService.activeOrgChanged.subscribe(org => { this.orgSub = this.authService.activeOrgChanged.subscribe(org => {
this.org = org; this.org = org;
this.getProjectCount();
}); });
this.authSub = this.authService.authenticationChanged.subscribe((authenticated) => { this.authSub = this.authService.authenticationChanged.subscribe((authenticated) => {
@@ -289,5 +211,15 @@ export class AppComponent implements OnDestroy {
this.authService.setActiveOrg(org); this.authService.setActiveOrg(org);
this.router.navigate(['/']); this.router.navigate(['/']);
} }
private async getProjectCount(): Promise<any> {
this.ownedProjectsCount = await this.projectService.SearchProjects(0, 0).then(res => {
return res.toObject().totalResult;
});
this.grantedProjectsCount = await this.projectService.SearchGrantedProjects(0, 0).then(res => {
return res.toObject().totalResult;
});
}
} }

View File

@@ -1,5 +1,5 @@
import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core'; import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';
import { AuthUserService } from 'src/app/services/auth-user.service'; import { AuthService } from 'src/app/services/auth.service';
@Directive({ @Directive({
@@ -10,7 +10,7 @@ export class HasRoleDirective {
private hasView: boolean = false; private hasView: boolean = false;
@Input() public set appHasRole(roles: string[]) { @Input() public set appHasRole(roles: string[]) {
if (roles && roles.length > 0) { if (roles && roles.length > 0) {
this.userService.isAllowed(roles).subscribe(isAllowed => { this.authService.isAllowed(roles).subscribe(isAllowed => {
if (isAllowed && !this.hasView) { if (isAllowed && !this.hasView) {
this.viewContainerRef.clear(); this.viewContainerRef.clear();
this.viewContainerRef.createEmbeddedView(this.templateRef); this.viewContainerRef.createEmbeddedView(this.templateRef);
@@ -23,7 +23,7 @@ export class HasRoleDirective {
} }
constructor( constructor(
private userService: AuthUserService, private authService: AuthService,
protected templateRef: TemplateRef<any>, protected templateRef: TemplateRef<any>,
protected viewContainerRef: ViewContainerRef, protected viewContainerRef: ViewContainerRef,
) { } ) { }

View File

@@ -2,19 +2,19 @@ import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot } from '@angular/router'; import { ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot } from '@angular/router';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { AuthUserService } from '../services/auth-user.service'; import { AuthService } from '../services/auth.service';
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
}) })
export class RoleGuard implements CanActivate { export class RoleGuard implements CanActivate {
constructor(private userService: AuthUserService) { } constructor(private authService: AuthService) { }
public canActivate( public canActivate(
route: ActivatedRouteSnapshot, route: ActivatedRouteSnapshot,
state: RouterStateSnapshot, state: RouterStateSnapshot,
): Observable<boolean> { ): Observable<boolean> {
return this.userService.isAllowed(route.data['roles'], true); return this.authService.isAllowed(route.data['roles'], true);
} }
} }

View File

@@ -15,9 +15,5 @@
box-sizing: border-box; box-sizing: border-box;
outline: none; outline: none;
color: white; color: white;
// &.active:hover {
// border: 2px solid #8795a1;
// }
} }
} }

View File

@@ -1,9 +1,6 @@
@import '~@angular/material/theming'; @import '~@angular/material/theming';
@mixin card-theme($theme) { @mixin card-theme($theme) {
$accent: map-get($theme, accent);
$background: map-get($theme, background);
$background-color: mat-color($background, card);
$primary: map-get($theme, primary); $primary: map-get($theme, primary);
$primary-color: mat-color($primary, 500); $primary-color: mat-color($primary, 500);
$primary-dark: mat-color($primary, A800); $primary-dark: mat-color($primary, A800);
@@ -17,7 +14,6 @@
box-sizing: border-box; box-sizing: border-box;
border-radius: .5rem; border-radius: .5rem;
outline: none; outline: none;
// 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);
.selection-icon { .selection-icon {
opacity: 0; opacity: 0;

View File

@@ -1,7 +1,8 @@
<span class="header">{{ 'CHANGES.LISTTITLE' | translate }}</span> <span class="header">{{ 'CHANGES.LISTTITLE' | translate }}</span>
<div class="scroll-container" appScrollable (scrollPosition)="scrollHandler($event)"> <div class="scroll-container" appScrollable (scrollPosition)="scrollHandler($event)"
<li class="item change-item-back" *ngFor="let event of data | async"> [@cardAnimation]="data && (data | async)?.length">
<li class="item change-item-back" *ngFor="let event of data | async" @animate>
<span class="seq"> <span class="seq">
{{event.changeDate | timestampToDate | localizedDate: 'EEE dd. MMM, HH:mm'}} {{event.changeDate | timestampToDate | localizedDate: 'EEE dd. MMM, HH:mm'}}
</span> </span>

View File

@@ -1,3 +1,6 @@
@import '~@angular/material/theming';
.header { .header {
display: block; display: block;
margin-bottom: 1rem; margin-bottom: 1rem;
@@ -5,42 +8,53 @@
margin-top: 1rem; margin-top: 1rem;
} }
.scroll-container { @mixin changes-theme($theme) {
max-height: 60vh;
overflow-y: scroll;
.item { .scroll-container {
box-sizing: border-box; max-height: 60vh;
padding: .5rem; overflow-y: scroll;
margin: .25rem 0;
border-radius: .5rem; .item {
display: flex; box-sizing: border-box;
flex-direction: column; padding: .5rem;
.editor { margin: .25rem 0;
color: #8795a1; border-radius: .5rem;
display: flex;
flex-direction: column;
.editor {
color: #8795a1;
font-size: 12px;
align-self: flex-end;
}
.seq {
color: #8795a1;
font-size: 12px;
align-self: flex-end;
}
.desc {
overflow-x: auto;
font-size: 14px;
}
$primary: map-get($theme, primary);
$primary-dark: mat-color($primary, A800);
&.change-item-back {
background-color: rgba($primary-dark, 0.93);
transition: background-color .4s ease-in-out;
}
}
.sp-wrapper {
padding: .5rem;
display: flex;
justify-content: center;
}
.end-container {
font-size: 12px; font-size: 12px;
align-self: flex-end;
}
.seq {
color: #8795a1; color: #8795a1;
font-size: 12px;
align-self: flex-end;
}
.desc {
overflow-x: auto;
font-size: 14px;
} }
} }
}
.sp-wrapper {
padding: .5rem;
display: flex;
justify-content: center;
}
.end-container {
font-size: 12px;
color: #8795a1;
}
}

View File

@@ -1,3 +1,4 @@
import { animate, animateChild, keyframes, query, stagger, style, transition, trigger } from '@angular/animations';
import { Component, Input, OnInit } from '@angular/core'; import { Component, Input, OnInit } from '@angular/core';
import { BehaviorSubject, from, Observable, of } from 'rxjs'; import { BehaviorSubject, from, Observable, of } from 'rxjs';
import { catchError, scan, take, tap } from 'rxjs/operators'; import { catchError, scan, take, tap } from 'rxjs/operators';
@@ -16,6 +17,22 @@ export enum ChangeType {
selector: 'app-changes', selector: 'app-changes',
templateUrl: './changes.component.html', templateUrl: './changes.component.html',
styleUrls: ['./changes.component.scss'], styleUrls: ['./changes.component.scss'],
animations: [
trigger('cardAnimation', [
transition('* => *', [
query('@animate', stagger('50ms', animateChild()), { optional: true }),
]),
]),
trigger('animate', [
transition(':enter', [
animate('.2s ease-in', keyframes([
style({ opacity: 0 }),
style({ opacity: .5, transform: 'scale(1.02)' }),
style({ opacity: 1, transform: 'scale(1)' }),
])),
]),
]),
],
}) })
export class ChangesComponent implements OnInit { export class ChangesComponent implements OnInit {
@Input() public changeType: ChangeType = ChangeType.USER; @Input() public changeType: ChangeType = ChangeType.USER;

View File

@@ -2,12 +2,12 @@
<span class="header">{{ title }}</span> <span class="header">{{ title }}</span>
<span class="sub-header">{{ description }}</span> <span class="sub-header">{{ description }}</span>
<div class="people"> <div class="people">
<div class="img-list"> <div class="img-list" [@cardAnimation]="totalResult">
<mat-spinner diameter="20" *ngIf="loading"></mat-spinner> <mat-spinner diameter="20" *ngIf="loading"></mat-spinner>
<ng-container *ngIf="totalResult < 10; else compact"> <ng-container *ngIf="totalResult < 10; else compact">
<ng-container *ngFor="let member of membersSubject | async; index as i"> <ng-container *ngFor="let member of membersSubject | async; index as i">
<div (click)="emitShowDetail()" class="avatar-circle" <div @animate (click)="emitShowDetail()" class="avatar-circle"
matTooltip="{{ member.email }} | {{member.rolesList?.join(' ')}}" matTooltip="{{ member.email }} | {{member.rolesList?.join(' ')}}"
[ngStyle]="{'z-index': 100 - i}"> [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))"

View File

@@ -1,4 +1,4 @@
import { animate, animateChild, query, stagger, style, transition, trigger } from '@angular/animations'; import { animate, animateChild, keyframes, query, stagger, style, transition, trigger } from '@angular/animations';
import { Component, EventEmitter, Input, Output } from '@angular/core'; import { Component, EventEmitter, Input, Output } from '@angular/core';
import { BehaviorSubject } from 'rxjs'; import { BehaviorSubject } from 'rxjs';
@@ -7,17 +7,18 @@ import { BehaviorSubject } from 'rxjs';
templateUrl: './contributors.component.html', templateUrl: './contributors.component.html',
styleUrls: ['./contributors.component.scss'], styleUrls: ['./contributors.component.scss'],
animations: [ animations: [
trigger('list', [ trigger('cardAnimation', [
transition(':enter', [ transition('* => *', [
query('@animate', query('@animate', stagger('40ms', animateChild()), { optional: true }),
stagger(80, animateChild()),
),
]), ]),
]), ]),
trigger('animate', [ trigger('animate', [
transition(':enter', [ transition(':enter', [
style({ opacity: 0, transform: 'translateX(100%)' }), animate('.2s ease-in', keyframes([
animate('100ms', style({ opacity: 1, transform: 'translateX(0)' })), style({ opacity: 0, offset: 0 }),
style({ opacity: .5, transform: 'scale(1.05)', offset: 0.3 }),
style({ opacity: 1, transform: 'scale(1)', offset: 1 }),
])),
]), ]),
]), ]),
], ],

View File

@@ -51,7 +51,7 @@
<ng-container matColumnDef="creationDate"> <ng-container matColumnDef="creationDate">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.ROLE.CREATIONDATE' | translate }} </th> <th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.ROLE.CREATIONDATE' | translate }} </th>
<td (click)="openDetailDialog(role)" mat-cell *matCellDef="let role"> <td (click)="openDetailDialog(role)" mat-cell *matCellDef="let role">
<span>{{role.creationDate | timestampToDate | date: 'dd. MMM, HH:mm' }}</span> <span>{{role.creationDate | timestampToDate | localizedDate: 'dd. MMM, HH:mm' }}</span>
</td> </td>
</ng-container> </ng-container>

View File

@@ -16,6 +16,7 @@ import { RouterModule } from '@angular/router';
import { TranslateModule } from '@ngx-translate/core'; 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 { LocalizedDatePipeModule } from 'src/app/pipes/localized-date-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 { RefreshTableModule } from '../refresh-table/refresh-table.module';
@@ -46,6 +47,7 @@ import { ProjectRolesComponent } from './project-roles.component';
MatMenuModule, MatMenuModule,
TimestampToDatePipeModule, TimestampToDatePipeModule,
RefreshTableModule, RefreshTableModule,
LocalizedDatePipeModule,
], ],
exports: [ exports: [
ProjectRolesComponent, ProjectRolesComponent,

View File

@@ -51,13 +51,13 @@
<ng-container matColumnDef="creationDate"> <ng-container matColumnDef="creationDate">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.GRANT.CREATIONDATE' | translate }} </th> <th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.GRANT.CREATIONDATE' | translate }} </th>
<td class="pointer" mat-cell *matCellDef="let grant"> <td class="pointer" mat-cell *matCellDef="let grant">
{{grant.creationDate | timestampToDate | date: 'dd. MMM, HH:mm' }} </td> {{grant.creationDate | timestampToDate | localizedDate: 'dd. MMM, HH:mm' }} </td>
</ng-container> </ng-container>
<ng-container matColumnDef="changeDate"> <ng-container matColumnDef="changeDate">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.GRANT.CHANGEDATE' | translate }} </th> <th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.GRANT.CHANGEDATE' | translate }} </th>
<td class="pointer" mat-cell *matCellDef="let grant"> <td class="pointer" mat-cell *matCellDef="let grant">
{{grant.changeDate | timestampToDate | date: 'dd. MMM, HH:mm' }} </td> {{grant.changeDate | timestampToDate | localizedDate: 'dd. MMM, HH:mm' }} </td>
</ng-container> </ng-container>
<ng-container matColumnDef="roleNamesList"> <ng-container matColumnDef="roleNamesList">

View File

@@ -14,6 +14,7 @@ import { RouterModule } from '@angular/router';
import { TranslateModule } from '@ngx-translate/core'; 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 { LocalizedDatePipeModule } from 'src/app/pipes/localized-date-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';
@@ -43,6 +44,7 @@ import { UserGrantsComponent } from './user-grants.component';
HasRolePipeModule, HasRolePipeModule,
TimestampToDatePipeModule, TimestampToDatePipeModule,
RefreshTableModule, RefreshTableModule,
LocalizedDatePipeModule,
], ],
exports: [ exports: [
UserGrantsComponent, UserGrantsComponent,

View File

@@ -14,12 +14,12 @@
<div class="text-part"> <div class="text-part">
<span *ngIf="item.changeDate" class="top">{{'PROJECT.PAGES.LASTMODIFIED' | translate}} <span *ngIf="item.changeDate" class="top">{{'PROJECT.PAGES.LASTMODIFIED' | translate}}
{{ {{
item.changeDate | timestampToDate | date: 'EEE dd. MMM, HH:mm' item.changeDate | timestampToDate | localizedDate: 'EEE dd. MMM, HH:mm'
}}</span> }}</span>
<span class="name" *ngIf="item.projectName">{{ item.projectName }}</span> <span class="name" *ngIf="item.projectName">{{ item.projectName }}</span>
<span class="description" *ngIf="item.resourceOwnerName">{{item.resourceOwnerName}}</span> <span class="description" *ngIf="item.resourceOwnerName">{{item.resourceOwnerName}}</span>
<span *ngIf="item.changeDate" class="created">{{'PROJECT.PAGES.CREATEDON' | translate}} <span *ngIf="item.changeDate" class="created">{{'PROJECT.PAGES.CREATEDON' | translate}}
{{ item.creationDate | timestampToDate | date: 'EEE dd. MMM, HH:mm' }}</span> {{ item.creationDate | timestampToDate | localizedDate: 'EEE dd. MMM, HH:mm' }}</span>
<span class="fill-space"></span> <span class="fill-space"></span>
<div class="icons"> <div class="icons">
</div> </div>
@@ -41,12 +41,12 @@
<div class="text-part"> <div class="text-part">
<span *ngIf="item.changeDate" class="top">{{'PROJECT.PAGES.LASTMODIFIED' | translate}} <span *ngIf="item.changeDate" class="top">{{'PROJECT.PAGES.LASTMODIFIED' | translate}}
{{ {{
item.changeDate | timestampToDate | date: 'EEE dd. MMM, HH:mm' item.changeDate | timestampToDate | localizedDate: 'EEE dd. MMM, HH:mm'
}}</span> }}</span>
<span class="name" *ngIf="item.projectName">{{ item.projectName }}</span> <span class="name" *ngIf="item.projectName">{{ item.projectName }}</span>
<span class="description" *ngIf="item.resourceOwnerName">{{item.resourceOwnerName}}</span> <span class="description" *ngIf="item.resourceOwnerName">{{item.resourceOwnerName}}</span>
<span *ngIf="item.changeDate" class="created">{{'PROJECT.PAGES.CREATEDON' | translate}} <span *ngIf="item.changeDate" class="created">{{'PROJECT.PAGES.CREATEDON' | translate}}
{{ item.creationDate | timestampToDate | date: 'EEE dd. MMM, HH:mm' }}</span> {{ item.creationDate | timestampToDate | localizedDate: 'EEE dd. MMM, HH:mm' }}</span>
<span class="fill-space"></span> <span class="fill-space"></span>
<div class="icons"> <div class="icons">
</div> </div>

View File

@@ -72,7 +72,7 @@
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.TABLE.CREATIONDATE' | translate }} </th> <th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.TABLE.CREATIONDATE' | translate }} </th>
<td mat-cell *matCellDef="let project"> <td mat-cell *matCellDef="let project">
<span <span
*ngIf="project.creationDate">{{project.creationDate | timestampToDate | date: 'EEE dd. MMM, HH:mm'}}</span> *ngIf="project.creationDate">{{project.creationDate | timestampToDate | localizedDate: 'EEE dd. MMM, HH:mm'}}</span>
</td> </td>
</ng-container> </ng-container>
@@ -81,7 +81,7 @@
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.TABLE.CHANGEDATE' | translate }} </th> <th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.TABLE.CHANGEDATE' | translate }} </th>
<td mat-cell *matCellDef="let project"> <td mat-cell *matCellDef="let project">
<span <span
*ngIf="project.changeDate">{{project.changeDate | timestampToDate | date: 'EEE dd. MMM, HH:mm'}}</span> *ngIf="project.changeDate">{{project.changeDate | timestampToDate | localizedDate: 'EEE dd. MMM, HH:mm'}}</span>
</td> </td>
</ng-container> </ng-container>

View File

@@ -25,6 +25,7 @@ import { ProjectRolesModule } from 'src/app/modules/project-roles/project-roles.
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';
import { LocalizedDatePipeModule } from 'src/app/pipes/localized-date-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 { GrantedProjectDetailComponent } from './granted-project-detail/granted-project-detail.component'; import { GrantedProjectDetailComponent } from './granted-project-detail/granted-project-detail.component';
@@ -70,6 +71,7 @@ import { GrantedProjectsComponent } from './granted-projects.component';
TranslateModule, TranslateModule,
TimestampToDatePipeModule, TimestampToDatePipeModule,
SharedModule, SharedModule,
LocalizedDatePipeModule,
MemberCreateDialogModule, MemberCreateDialogModule,
], ],
}) })

View File

@@ -23,6 +23,7 @@ import { RefreshTableModule } from 'src/app/modules/refresh-table/refresh-table.
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';
import { LocalizedDatePipeModule } from 'src/app/pipes/localized-date-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 { ApplicationGridComponent } from './application-grid/application-grid.component'; import { ApplicationGridComponent } from './application-grid/application-grid.component';
@@ -65,6 +66,7 @@ import { ProjectGrantsComponent } from './project-grants/project-grants.componen
MetaLayoutModule, MetaLayoutModule,
RefreshTableModule, RefreshTableModule,
MemberCreateDialogModule, MemberCreateDialogModule,
LocalizedDatePipeModule,
], ],
}) })
export class OwnedProjectDetailModule { } export class OwnedProjectDetailModule { }

View File

@@ -43,14 +43,14 @@
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.GRANT.CREATIONDATE' | translate }} </th> <th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.GRANT.CREATIONDATE' | translate }} </th>
<td [routerLink]="['/projects',grant.projectId,'grant', grant.id]" class="pointer" mat-cell <td [routerLink]="['/projects',grant.projectId,'grant', grant.id]" class="pointer" mat-cell
*matCellDef="let grant"> *matCellDef="let grant">
{{grant.creationDate | timestampToDate | date: 'dd. MMM, HH:mm' }} </td> {{grant.creationDate | timestampToDate | localizedDate: 'dd. MMM, HH:mm' }} </td>
</ng-container> </ng-container>
<ng-container matColumnDef="changeDate"> <ng-container matColumnDef="changeDate">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.GRANT.CHANGEDATE' | translate }} </th> <th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.GRANT.CHANGEDATE' | translate }} </th>
<td [routerLink]="['/projects',grant.projectId,'grant', grant.id]" class="pointer" mat-cell <td [routerLink]="['/projects',grant.projectId,'grant', grant.id]" class="pointer" mat-cell
*matCellDef="let grant"> *matCellDef="let grant">
{{grant.changeDate | timestampToDate | date: 'dd. MMM, HH:mm' }} </td> {{grant.changeDate | timestampToDate | localizedDate: 'dd. MMM, HH:mm' }} </td>
</ng-container> </ng-container>

View File

@@ -15,13 +15,13 @@
<div class="text-part"> <div class="text-part">
<span *ngIf="item.changeDate" class="top">{{'PROJECT.PAGES.LASTMODIFIED' | translate}} <span *ngIf="item.changeDate" class="top">{{'PROJECT.PAGES.LASTMODIFIED' | translate}}
{{ {{
item.changeDate | timestampToDate | date: 'EEE dd. MMM, HH:mm' item.changeDate | timestampToDate | localizedDate: 'EEE dd. MMM, HH:mm'
}}</span> }}</span>
<span class="name" *ngIf="item.name">{{ item.name }}</span> <span class="name" *ngIf="item.name">{{ item.name }}</span>
<span *ngIf="item.changeDate" class="created">{{'PROJECT.PAGES.CREATEDON' | translate}} <span *ngIf="item.changeDate" class="created">{{'PROJECT.PAGES.CREATEDON' | translate}}
{{ {{
item.creationDate | timestampToDate | date: 'EEE dd. MMM, HH:mm' item.creationDate | timestampToDate | localizedDate: 'EEE dd. MMM, HH:mm'
}}</span> }}</span>
<span class="fill-space"></span> <span class="fill-space"></span>
<div class="icons"> <div class="icons">
@@ -44,13 +44,13 @@
<div class="text-part"> <div class="text-part">
<span *ngIf="item.changeDate" class="top">{{'PROJECT.PAGES.LASTMODIFIED' | translate}} <span *ngIf="item.changeDate" class="top">{{'PROJECT.PAGES.LASTMODIFIED' | translate}}
{{ {{
item.changeDate | timestampToDate | date: 'EEE dd. MMM, HH:mm' item.changeDate | timestampToDate | localizedDate: 'EEE dd. MMM, HH:mm'
}}</span> }}</span>
<span class="name" *ngIf="item.name">{{ item.name }}</span> <span class="name" *ngIf="item.name">{{ item.name }}</span>
<span *ngIf="item.changeDate" class="created">{{'PROJECT.PAGES.CREATEDON' | translate}} <span *ngIf="item.changeDate" class="created">{{'PROJECT.PAGES.CREATEDON' | translate}}
{{ {{
item.creationDate | timestampToDate | date: 'EEE dd. MMM, HH:mm' item.creationDate | timestampToDate | localizedDate: 'EEE dd. MMM, HH:mm'
}}</span> }}</span>
<span class="fill-space"></span> <span class="fill-space"></span>
<div class="icons"> <div class="icons">

View File

@@ -1,4 +1,4 @@
import { animate, animateChild, query, stagger, style, transition, trigger } from '@angular/animations'; import { animate, animateChild, keyframes, query, stagger, style, transition, trigger } from '@angular/animations';
import { SelectionModel } from '@angular/cdk/collections'; import { SelectionModel } from '@angular/cdk/collections';
import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core'; import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
@@ -10,21 +10,25 @@ import { AuthService } from 'src/app/services/auth.service';
templateUrl: './owned-project-grid.component.html', templateUrl: './owned-project-grid.component.html',
styleUrls: ['./owned-project-grid.component.scss'], styleUrls: ['./owned-project-grid.component.scss'],
animations: [ animations: [
trigger('list', [ trigger('cardAnimation', [
transition(':enter', [ transition('* => *', [
query('@animate', query('@animate', stagger('100ms', animateChild()), { optional: true }),
stagger(100, animateChild()),
),
]), ]),
]), ]),
trigger('animate', [ trigger('animate', [
transition(':enter', [ transition(':enter', [
style({ opacity: 0, transform: 'translateY(-100%)' }), animate('.2s ease-in', keyframes([
animate('100ms', style({ opacity: 1, transform: 'translateY(0)' })), style({ opacity: 0, transform: 'translateY(-50%)', offset: 0 }),
style({ opacity: .5, transform: 'translateY(-10px) scale(1.1)', offset: 0.3 }),
style({ opacity: 1, transform: 'translateY(0)', offset: 1 }),
])),
]), ]),
transition(':leave', [ transition(':leave', [
style({ opacity: 1, transform: 'translateY(0)' }), animate('.2s ease-out', keyframes([
animate('100ms', style({ opacity: 0, transform: 'translateY(100%)' })), style({ opacity: 1, transform: 'scale(1.1)', offset: 0 }),
style({ opacity: .5, transform: 'scale(.5)', offset: 0.3 }),
style({ opacity: 0, transform: 'scale(0)', offset: 1 }),
])),
]), ]),
]), ]),
], ],

View File

@@ -61,7 +61,7 @@
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.TABLE.CREATIONDATE' | translate }} </th> <th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.TABLE.CREATIONDATE' | translate }} </th>
<td mat-cell *matCellDef="let project"> <td mat-cell *matCellDef="let project">
<span <span
*ngIf="project.creationDate">{{project.creationDate | timestampToDate | date: 'EEE dd. MMM, HH:mm'}}</span> *ngIf="project.creationDate">{{project.creationDate | timestampToDate | localizedDate: 'EEE dd. MMM, HH:mm'}}</span>
</td> </td>
</ng-container> </ng-container>
@@ -70,7 +70,7 @@
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.TABLE.CHANGEDATE' | translate }} </th> <th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.TABLE.CHANGEDATE' | translate }} </th>
<td mat-cell *matCellDef="let project"> <td mat-cell *matCellDef="let project">
<span <span
*ngIf="project.changeDate">{{project.changeDate | timestampToDate | date: 'EEE dd. MMM, HH:mm'}}</span> *ngIf="project.changeDate">{{project.changeDate | timestampToDate | localizedDate: 'EEE dd. MMM, HH:mm'}}</span>
</td> </td>
</ng-container> </ng-container>

View File

@@ -88,7 +88,7 @@ export class OwnedProjectListComponent implements OnInit, OnDestroy {
this.projectService.SearchProjects(limit, offset).then(res => { this.projectService.SearchProjects(limit, offset).then(res => {
this.ownedProjectList = res.toObject().resultList; this.ownedProjectList = res.toObject().resultList;
this.totalResult = res.toObject().totalResult; this.totalResult = res.toObject().totalResult;
if (this.totalResult > 5) { if (this.totalResult > 10) {
this.grid = false; this.grid = false;
} }
this.dataSource.data = this.ownedProjectList; this.dataSource.data = this.ownedProjectList;

View File

@@ -20,6 +20,7 @@ import { CardModule } from 'src/app/modules/card/card.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';
import { LocalizedDatePipeModule } from 'src/app/pipes/localized-date-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 { OwnedProjectGridComponent } from './owned-project-list/owned-project-grid/owned-project-grid.component'; import { OwnedProjectGridComponent } from './owned-project-list/owned-project-grid/owned-project-grid.component';
@@ -58,6 +59,7 @@ import { OwnedProjectsComponent } from './owned-projects.component';
MatSortModule, MatSortModule,
HasRolePipeModule, HasRolePipeModule,
TimestampToDatePipeModule, TimestampToDatePipeModule,
LocalizedDatePipeModule,
SharedModule, SharedModule,
], ],
}) })

View File

@@ -4,7 +4,7 @@
$primary: map-get($theme, primary); $primary: map-get($theme, primary);
$primary-dark: mat-color($primary, A800); $primary-dark: mat-color($primary, A800);
.theme-conent, .theme-app , .crescent { .theme-conent, .crescent {
background-color: $primary-dark; background-color: $primary-dark;
transition: background-color .4s cubic-bezier(0.645, 0.045, 0.355, 1); transition: background-color .4s cubic-bezier(0.645, 0.045, 0.355, 1);
} }

View File

@@ -102,8 +102,8 @@ label {
} }
[type="checkbox"]:checked + .theme-app{ [type="checkbox"]:checked + .theme-app{
background-color: $dark-background; background-color: inherit;
color: $light-background; color: inherit;
} }
[type="checkbox"]:checked + .theme-app .crescent{ [type="checkbox"]:checked + .theme-app .crescent{

View File

@@ -38,10 +38,8 @@
<ng-template appHasRole [appHasRole]="['user.read', 'user.read:'+user?.id]"> <ng-template appHasRole [appHasRole]="['user.read', 'user.read:'+user?.id]">
<app-card title="{{ 'USER.PROFILE.TITLE' | translate }}"> <app-card title="{{ 'USER.PROFILE.TITLE' | translate }}">
<app-detail-form <app-detail-form [disabled]="(['user.write:' + user?.id, 'user.write'] | hasRole | async) == false"
*ngIf="((authUserService.isAllowed(['user.write:' + user?.id, 'user.write']) | async) || false) as canwrite" [genders]="genders" [languages]="languages" [profile]="user" (submitData)="saveProfile($event)">
[disabled]="canwrite" [genders]="genders" [languages]="languages" [profile]="user"
(submitData)="saveProfile($event)">
</app-detail-form> </app-detail-form>
</app-card> </app-card>
</ng-template> </ng-template>

View File

@@ -44,7 +44,6 @@ 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 => {
console.log(mfas.toObject().mfasList);
this.dataSource = new MatTableDataSource(mfas.toObject().mfasList); this.dataSource = new MatTableDataSource(mfas.toObject().mfasList);
this.dataSource.sort = this.sort; this.dataSource.sort = this.sort;
}).catch(error => { }).catch(error => {

View File

@@ -1,16 +1,16 @@
import { Pipe, PipeTransform } from '@angular/core'; import { Pipe, PipeTransform } from '@angular/core';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { AuthUserService } from '../services/auth-user.service'; import { AuthService } from '../services/auth.service';
@Pipe({ @Pipe({
name: 'hasRole', name: 'hasRole',
}) })
export class HasRolePipe implements PipeTransform { export class HasRolePipe implements PipeTransform {
constructor(private authUserService: AuthUserService) { } constructor(private authService: AuthService) { }
public transform(values: string[], each: boolean = false): Observable<boolean> { public transform(values: string[], each: boolean = false): Observable<boolean> {
return this.authUserService.isAllowed(values, each); return this.authService.isAllowed(values, each);
} }
} }

View File

@@ -1,8 +1,6 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Empty } from 'google-protobuf/google/protobuf/empty_pb'; import { Empty } from 'google-protobuf/google/protobuf/empty_pb';
import { Metadata } from 'grpc-web'; import { Metadata } from 'grpc-web';
import { from, Observable, of } from 'rxjs';
import { catchError, switchMap } from 'rxjs/operators';
import { AuthServicePromiseClient } from '../proto/generated/auth_grpc_web_pb'; import { AuthServicePromiseClient } from '../proto/generated/auth_grpc_web_pb';
import { import {
@@ -11,6 +9,7 @@ import {
Gender, Gender,
MfaOtpResponse, MfaOtpResponse,
MultiFactors, MultiFactors,
MyPermissions,
MyProjectOrgSearchQuery, MyProjectOrgSearchQuery,
MyProjectOrgSearchRequest, MyProjectOrgSearchRequest,
MyProjectOrgSearchResponse, MyProjectOrgSearchResponse,
@@ -38,8 +37,6 @@ import { GrpcService, RequestFactory, ResponseMapper } from './grpc.service';
providedIn: 'root', providedIn: 'root',
}) })
export class AuthUserService { export class AuthUserService {
private _roleCache: string[] = [];
constructor(private readonly grpcClient: GrpcService, constructor(private readonly grpcClient: GrpcService,
private grpcBackendService: GrpcBackendService, private grpcBackendService: GrpcBackendService,
) { } ) { }
@@ -75,7 +72,6 @@ export class AuthUserService {
); );
} }
public async GetMyUser(): Promise<UserView> { public async GetMyUser(): Promise<UserView> {
return await this.request( return await this.request(
c => c.getMyUser, c => c.getMyUser,
@@ -175,7 +171,7 @@ export class AuthUserService {
); );
} }
private async getMyzitadelPermissions(): Promise<any> { public async GetMyzitadelPermissions(): Promise<MyPermissions> {
return await this.request( return await this.request(
c => c.getMyZitadelPermissions, c => c.getMyZitadelPermissions,
new Empty(), new Empty(),
@@ -183,19 +179,6 @@ export class AuthUserService {
); );
} }
public GetMyzitadelPermissions(): Observable<any> {
return from(this.getMyzitadelPermissions());
}
public hasRoles(userRoles: string[], requestedRoles: string[], each: boolean = false): boolean {
return each ?
requestedRoles.every(role => userRoles.includes(role)) :
requestedRoles.findIndex(role => {
return userRoles.findIndex(i => i.includes(role)) > -1;
// return userRoles.includes(role);
}) > -1;
}
public async GetMyUserPhone(): Promise<UserPhone> { public async GetMyUserPhone(): Promise<UserPhone> {
// return this.grpcClient.auth.getMyUserPhone(new Empty()); // return this.grpcClient.auth.getMyUserPhone(new Empty());
return await this.request( return await this.request(
@@ -312,31 +295,4 @@ export class AuthUserService {
f => f, f => f,
); );
} }
public isAllowed(roles: string[], each: boolean = false): Observable<boolean> {
if (roles && roles.length > 0) {
if (this._roleCache.length > 0) {
return of(this.hasRoles(this._roleCache, roles));
}
return this.GetMyzitadelPermissions().pipe(
switchMap(response => {
let userRoles = [];
if (response.toObject().permissionsList) {
userRoles = response.toObject().permissionsList;
} else {
userRoles = ['user.resourceowner'];
}
this._roleCache = userRoles;
return of(this.hasRoles(userRoles, roles, each));
}),
catchError((err) => {
return of(false);
}),
);
} else {
return of(false);
}
}
} }

View File

@@ -2,7 +2,7 @@ import { Injectable } from '@angular/core';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { AuthConfig, OAuthService } from 'angular-oauth2-oidc'; import { AuthConfig, OAuthService } from 'angular-oauth2-oidc';
import { BehaviorSubject, from, merge, Observable, of, Subject } from 'rxjs'; import { BehaviorSubject, from, merge, Observable, of, Subject } from 'rxjs';
import { catchError, filter, map, mergeMap, take, timeout } from 'rxjs/operators'; import { catchError, filter, finalize, first, map, mergeMap, switchMap, take, timeout } from 'rxjs/operators';
import { Org, UserProfileView } from '../proto/generated/auth_pb'; import { Org, UserProfileView } from '../proto/generated/auth_pb';
import { AuthUserService } from './auth-user.service'; import { AuthUserService } from './auth-user.service';
@@ -22,6 +22,8 @@ export class AuthService {
boolean boolean
> = new BehaviorSubject(this.authenticated); > = new BehaviorSubject(this.authenticated);
private zitadelPermissions: BehaviorSubject<string[]> = new BehaviorSubject(['user.resourceowner']);
constructor( constructor(
private grpcService: GrpcService, private grpcService: GrpcService,
private config: AuthConfig, private config: AuthConfig,
@@ -44,9 +46,50 @@ export class AuthService {
).pipe( ).pipe(
take(1), take(1),
mergeMap(token => { mergeMap(token => {
return from(this.userService.GetMyUserProfile()).pipe(map(userprofile => userprofile.toObject())); console.log(token);
return from(this.userService.GetMyUserProfile().then(userprofile => userprofile.toObject()));
}),
finalize(() => {
this.loadPermissions();
}), }),
); );
this.activeOrgChanged.subscribe(() => {
console.log('org change');
this.loadPermissions();
});
}
private loadPermissions(): void {
console.log('load permissions');
merge([
// this.authenticationChanged,
this.activeOrgChanged.pipe(map(org => !!org)),
]).pipe(
first(),
switchMap(() => from(this.userService.GetMyzitadelPermissions())),
map(rolesResp => rolesResp.toObject().permissionsList),
).subscribe(roles => this.zitadelPermissions.next(roles));
}
public isAllowed(roles: string[], each: boolean = false): Observable<boolean> {
if (roles && roles.length > 0) {
return this.zitadelPermissions.pipe(switchMap(zroles => {
return of(this.hasRoles(zroles, roles, each));
}));
} else {
return of(false);
}
}
public hasRoles(userRoles: string[], requestedRoles: string[], each: boolean = false): boolean {
// console.log('has', userRoles);
// console.log('needs', requestedRoles);
return each ?
requestedRoles.every(role => userRoles.includes(role)) :
requestedRoles.findIndex(role => {
return userRoles.findIndex(i => i.includes(role)) > -1;
}) > -1;
} }
public get authenticated(): boolean { public get authenticated(): boolean {

View File

@@ -1,8 +1,8 @@
@import './styles/card'; @import 'src/app/modules/card/card';
@import './styles/table'; @import './styles/table';
@import './styles/sidenav-list'; @import './styles/sidenav-list';
@import 'src/app/modules/avatar/avatar.component'; @import 'src/app/modules/avatar/avatar.component';
@import './styles/changes'; @import 'src/app/modules/changes/changes.component';
@import 'src/app/pages/projects/owned-projects/owned-project-detail/application-grid/application-grid.component'; @import 'src/app/pages/projects/owned-projects/owned-project-detail/application-grid/application-grid.component';
@import './styles/meta'; @import './styles/meta';
@import 'src/app/pages/users/user-detail/auth-user-detail/theme-setting/theme-card'; @import 'src/app/pages/users/user-detail/auth-user-detail/theme-setting/theme-card';

View File

@@ -1,11 +0,0 @@
@import '~@angular/material/theming';
@mixin changes-theme($theme) {
$primary: map-get($theme, primary);
$primary-dark: mat-color($primary, A800);
.change-item-back {
background-color: rgba($primary-dark, 0.93);
transition: background-color .4s ease-in-out;
}
}

View File

@@ -6,9 +6,9 @@
$primary-color: mat-color($primary, 500); $primary-color: mat-color($primary, 500);
$accent-color: mat-color($accent, 500); $accent-color: mat-color($accent, 500);
$primary-dark: mat-color($primary, A900); $primary-dark: mat-color($primary, A900);
$inverse-color: mat-color($primary, A600);
$sec-dark: mat-color($primary, A800); $sec-dark: mat-color($primary, A800);
.mat-menu-item { .mat-menu-item {
&.show-all { &.show-all {
height: 2rem; height: 2rem;
@@ -30,6 +30,15 @@
color: $primary-color !important; color: $primary-color !important;
background-color: rgba($color: $primary-color, $alpha: 0.1) !important; background-color: rgba($color: $primary-color, $alpha: 0.1) !important;
} }
.c_label {
.count {
background-color: $primary-color;
padding: 3px 6px;
border-radius: 50vw;
color: white;
}
}
} }
.mat-menu-content, .mat-menu-panel { .mat-menu-content, .mat-menu-panel {