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="((['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()">
<mat-icon aria-label="Side nav toggle icon">menu</mat-icon>
</button>
@@ -53,7 +53,7 @@
<div class="side-column">
<div class="list">
<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']">
<i class="icon las la-user-circle"></i>
<span class="label">{{ 'MENU.PERSONAL_INFO' | translate }}</span>
@@ -64,52 +64,65 @@
<div class="divider">
<div class="line"></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>
<span class="label">{{'MENU.IAM' | translate}}</span>
</a>
</ng-container>
<div *ngIf="org" [@navAnimation]="org">
<ng-template appHasRole [appHasRole]="['org.read']">
<a class="nav-item" [routerLinkActive]="['active']" [routerLink]="[ '/org']">
<a @navitem class="nav-item" [routerLinkActive]="['active']" [routerLink]="[ '/org']">
<i class="icon las la-archway"></i>
<span class="label">{{org?.name ? org.name : 'MENU.ORGANIZATION' | translate}}</span>
<span
class="label">{{org?.name ? org.name : 'MENU.ORGANIZATION' | translate}}</span>
</a>
</ng-template>
<ng-template appHasRole [appHasRole]="['project.read']">
<div class="divider">
<div @navitem class="divider">
<div class="line"></div>
<span>{{'MENU.PROJECTSSECTION' | translate}}</span>
<div class="line"></div>
</div>
<a class="nav-item" [routerLinkActive]="['active']" [routerLink]="[ '/projects']">
<a @navitem class="nav-item" [routerLinkActive]="['active']"
[routerLink]="[ '/projects']">
<i class="icon las la-layer-group"></i>
<span class="label">{{org?.name ? org.name : 'MENU.ORGANIZATION' | translate}}
<div class="c_label">
<span>{{org?.name ? org.name : 'MENU.ORGANIZATION' | translate}}
{{'MENU.PROJECT' | translate}} </span>
<span *ngIf="ownedProjectsCount as ownedPCount"
class="count">{{ownedPCount}}</span>
</div>
</a>
<a class="nav-item" [routerLinkActive]="['active']" [routerLink]="[ '/granted-projects']">
<a @navitem *ngIf="grantedProjectsCount as grantPCount" class="nav-item"
[routerLinkActive]="['active']" [routerLink]="[ '/granted-projects']">
<i class="icon las la-layer-group"></i>
<span class="label">{{ 'MENU.GRANTEDPROJECT' | translate }}</span>
<div class="c_label">
<span>{{ 'MENU.GRANTEDPROJECT' | translate }}</span>
<span class="count">{{grantPCount}}</span>
</div>
</a>
</ng-template>
<ng-template appHasRole [appHasRole]="['user.read']">
<div class="divider">
<div @navitem class="divider">
<div class="line"></div>
<span class="label">
{{ 'MENU.USERSECTION' | translate }}</span>
<div class="line"></div>
</div>
<a class="nav-item" [routerLinkActive]="['active']" [routerLink]="[ '/users/all']"
[routerLinkActiveOptions]="{ exact: true }">
<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>
</div>
@@ -117,7 +130,7 @@
</div>
</mat-drawer>
<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>
</div>
<div class="router" [@routeAnimations]="prepareRoute(outlet)">

View File

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

View File

@@ -1,4 +1,3 @@
import { animate, group, query, style, transition, trigger } from '@angular/animations';
import { BreakpointObserver } from '@angular/cdk/layout';
import { OverlayContainer } from '@angular/cdk/overlay';
import { ViewportScroller } from '@angular/common';
@@ -11,9 +10,11 @@ import { TranslateService } from '@ngx-translate/core';
import { Observable, of, Subscription } from 'rxjs';
import { map } from 'rxjs/operators';
import { accountCard, navAnimations, routeAnimations, toolbarAnimation } from './animations';
import { Org, UserProfileView } from './proto/generated/auth_pb';
import { AuthUserService } from './services/auth-user.service';
import { AuthService } from './services/auth.service';
import { ProjectService } from './services/project.service';
import { ThemeService } from './services/theme.service';
import { ToastService } from './services/toast.service';
import { UpdateService } from './services/update.service';
@@ -23,97 +24,10 @@ import { UpdateService } from './services/update.service';
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
animations: [
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,
}),
),
]),
]),
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,
},
),
]),
]),
]),
toolbarAnimation,
...navAnimations,
accountCard,
routeAnimations,
],
})
export class AppComponent implements OnDestroy {
@@ -135,6 +49,10 @@ export class AppComponent implements OnDestroy {
public orgLoading: boolean = false;
public showProjectSection: boolean = false;
public grantedProjectsCount: number = 0;
public ownedProjectsCount: number = 0;
private authSub: Subscription = new Subscription();
private orgSub: Subscription = new Subscription();
@@ -147,6 +65,7 @@ export class AppComponent implements OnDestroy {
public overlayContainer: OverlayContainer,
private themeService: ThemeService,
public userService: AuthUserService,
private projectService: ProjectService,
public matIconRegistry: MatIconRegistry,
public domSanitizer: DomSanitizer,
private toast: ToastService,
@@ -218,9 +137,12 @@ export class AppComponent implements OnDestroy {
'mdi_pin',
this.domSanitizer.bypassSecurityTrustResourceUrl('assets/mdi/pin.svg'),
);
this.getProjectCount();
this.orgSub = this.authService.activeOrgChanged.subscribe(org => {
this.org = org;
this.getProjectCount();
});
this.authSub = this.authService.authenticationChanged.subscribe((authenticated) => {
@@ -289,5 +211,15 @@ export class AppComponent implements OnDestroy {
this.authService.setActiveOrg(org);
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 { AuthUserService } from 'src/app/services/auth-user.service';
import { AuthService } from 'src/app/services/auth.service';
@Directive({
@@ -10,7 +10,7 @@ export class HasRoleDirective {
private hasView: boolean = false;
@Input() public set appHasRole(roles: string[]) {
if (roles && roles.length > 0) {
this.userService.isAllowed(roles).subscribe(isAllowed => {
this.authService.isAllowed(roles).subscribe(isAllowed => {
if (isAllowed && !this.hasView) {
this.viewContainerRef.clear();
this.viewContainerRef.createEmbeddedView(this.templateRef);
@@ -23,7 +23,7 @@ export class HasRoleDirective {
}
constructor(
private userService: AuthUserService,
private authService: AuthService,
protected templateRef: TemplateRef<any>,
protected viewContainerRef: ViewContainerRef,
) { }

View File

@@ -2,19 +2,19 @@ import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot } from '@angular/router';
import { Observable } from 'rxjs';
import { AuthUserService } from '../services/auth-user.service';
import { AuthService } from '../services/auth.service';
@Injectable({
providedIn: 'root',
})
export class RoleGuard implements CanActivate {
constructor(private userService: AuthUserService) { }
constructor(private authService: AuthService) { }
public canActivate(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot,
): 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;
outline: none;
color: white;
// &.active:hover {
// border: 2px solid #8795a1;
// }
}
}

View File

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

View File

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

View File

@@ -1,3 +1,6 @@
@import '~@angular/material/theming';
.header {
display: block;
margin-bottom: 1rem;
@@ -5,6 +8,8 @@
margin-top: 1rem;
}
@mixin changes-theme($theme) {
.scroll-container {
max-height: 60vh;
overflow-y: scroll;
@@ -31,6 +36,14 @@
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 {
@@ -44,3 +57,4 @@
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 { BehaviorSubject, from, Observable, of } from 'rxjs';
import { catchError, scan, take, tap } from 'rxjs/operators';
@@ -16,6 +17,22 @@ export enum ChangeType {
selector: 'app-changes',
templateUrl: './changes.component.html',
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 {
@Input() public changeType: ChangeType = ChangeType.USER;

View File

@@ -2,12 +2,12 @@
<span class="header">{{ title }}</span>
<span class="sub-header">{{ description }}</span>
<div class="people">
<div class="img-list">
<div class="img-list" [@cardAnimation]="totalResult">
<mat-spinner diameter="20" *ngIf="loading"></mat-spinner>
<ng-container *ngIf="totalResult < 10; else compact">
<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(' ')}}"
[ngStyle]="{'z-index': 100 - i}">
<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 { BehaviorSubject } from 'rxjs';
@@ -7,17 +7,18 @@ import { BehaviorSubject } from 'rxjs';
templateUrl: './contributors.component.html',
styleUrls: ['./contributors.component.scss'],
animations: [
trigger('list', [
transition(':enter', [
query('@animate',
stagger(80, animateChild()),
),
trigger('cardAnimation', [
transition('* => *', [
query('@animate', stagger('40ms', animateChild()), { optional: true }),
]),
]),
trigger('animate', [
transition(':enter', [
style({ opacity: 0, transform: 'translateX(100%)' }),
animate('100ms', style({ opacity: 1, transform: 'translateX(0)' })),
animate('.2s ease-in', keyframes([
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">
<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>
<span>{{role.creationDate | timestampToDate | localizedDate: 'dd. MMM, HH:mm' }}</span>
</td>
</ng-container>

View File

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

View File

@@ -51,13 +51,13 @@
<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>
{{grant.creationDate | timestampToDate | localizedDate: '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>
{{grant.changeDate | timestampToDate | localizedDate: 'dd. MMM, HH:mm' }} </td>
</ng-container>
<ng-container matColumnDef="roleNamesList">

View File

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

View File

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

View File

@@ -72,7 +72,7 @@
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.TABLE.CREATIONDATE' | translate }} </th>
<td mat-cell *matCellDef="let project">
<span
*ngIf="project.creationDate">{{project.creationDate | timestampToDate | date: 'EEE dd. MMM, HH:mm'}}</span>
*ngIf="project.creationDate">{{project.creationDate | timestampToDate | localizedDate: 'EEE dd. MMM, HH:mm'}}</span>
</td>
</ng-container>
@@ -81,7 +81,7 @@
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.TABLE.CHANGEDATE' | translate }} </th>
<td mat-cell *matCellDef="let project">
<span
*ngIf="project.changeDate">{{project.changeDate | timestampToDate | date: 'EEE dd. MMM, HH:mm'}}</span>
*ngIf="project.changeDate">{{project.changeDate | timestampToDate | localizedDate: 'EEE dd. MMM, HH:mm'}}</span>
</td>
</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 { UserGrantsModule } from 'src/app/modules/user-grants/user-grants.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 { GrantedProjectDetailComponent } from './granted-project-detail/granted-project-detail.component';
@@ -70,6 +71,7 @@ import { GrantedProjectsComponent } from './granted-projects.component';
TranslateModule,
TimestampToDatePipeModule,
SharedModule,
LocalizedDatePipeModule,
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 { WarnDialogModule } from 'src/app/modules/warn-dialog/warn-dialog.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 { ApplicationGridComponent } from './application-grid/application-grid.component';
@@ -65,6 +66,7 @@ import { ProjectGrantsComponent } from './project-grants/project-grants.componen
MetaLayoutModule,
RefreshTableModule,
MemberCreateDialogModule,
LocalizedDatePipeModule,
],
})
export class OwnedProjectDetailModule { }

View File

@@ -43,14 +43,14 @@
<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>
{{grant.creationDate | timestampToDate | localizedDate: '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>
{{grant.changeDate | timestampToDate | localizedDate: 'dd. MMM, HH:mm' }} </td>
</ng-container>

View File

@@ -15,13 +15,13 @@
<div class="text-part">
<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 class="name" *ngIf="item.name">{{ item.name }}</span>
<span *ngIf="item.changeDate" class="created">{{'PROJECT.PAGES.CREATEDON' | translate}}
{{
item.creationDate | timestampToDate | date: 'EEE dd. MMM, HH:mm'
item.creationDate | timestampToDate | localizedDate: 'EEE dd. MMM, HH:mm'
}}</span>
<span class="fill-space"></span>
<div class="icons">
@@ -44,13 +44,13 @@
<div class="text-part">
<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 class="name" *ngIf="item.name">{{ item.name }}</span>
<span *ngIf="item.changeDate" class="created">{{'PROJECT.PAGES.CREATEDON' | translate}}
{{
item.creationDate | timestampToDate | date: 'EEE dd. MMM, HH:mm'
item.creationDate | timestampToDate | localizedDate: 'EEE dd. MMM, HH:mm'
}}</span>
<span class="fill-space"></span>
<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 { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core';
import { Router } from '@angular/router';
@@ -10,21 +10,25 @@ import { AuthService } from 'src/app/services/auth.service';
templateUrl: './owned-project-grid.component.html',
styleUrls: ['./owned-project-grid.component.scss'],
animations: [
trigger('list', [
transition(':enter', [
query('@animate',
stagger(100, animateChild()),
),
trigger('cardAnimation', [
transition('* => *', [
query('@animate', stagger('100ms', animateChild()), { optional: true }),
]),
]),
trigger('animate', [
transition(':enter', [
style({ opacity: 0, transform: 'translateY(-100%)' }),
animate('100ms', style({ opacity: 1, transform: 'translateY(0)' })),
animate('.2s ease-in', keyframes([
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', [
style({ opacity: 1, transform: 'translateY(0)' }),
animate('100ms', style({ opacity: 0, transform: 'translateY(100%)' })),
animate('.2s ease-out', keyframes([
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>
<td mat-cell *matCellDef="let project">
<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>
</ng-container>
@@ -70,7 +70,7 @@
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.TABLE.CHANGEDATE' | translate }} </th>
<td mat-cell *matCellDef="let project">
<span
*ngIf="project.changeDate">{{project.changeDate | timestampToDate | date: 'EEE dd. MMM, HH:mm'}}</span>
*ngIf="project.changeDate">{{project.changeDate | timestampToDate | localizedDate: 'EEE dd. MMM, HH:mm'}}</span>
</td>
</ng-container>

View File

@@ -88,7 +88,7 @@ export class OwnedProjectListComponent implements OnInit, OnDestroy {
this.projectService.SearchProjects(limit, offset).then(res => {
this.ownedProjectList = res.toObject().resultList;
this.totalResult = res.toObject().totalResult;
if (this.totalResult > 5) {
if (this.totalResult > 10) {
this.grid = false;
}
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 { UserGrantsModule } from 'src/app/modules/user-grants/user-grants.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 { OwnedProjectGridComponent } from './owned-project-list/owned-project-grid/owned-project-grid.component';
@@ -58,6 +59,7 @@ import { OwnedProjectsComponent } from './owned-projects.component';
MatSortModule,
HasRolePipeModule,
TimestampToDatePipeModule,
LocalizedDatePipeModule,
SharedModule,
],
})

View File

@@ -4,7 +4,7 @@
$primary: map-get($theme, primary);
$primary-dark: mat-color($primary, A800);
.theme-conent, .theme-app , .crescent {
.theme-conent, .crescent {
background-color: $primary-dark;
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{
background-color: $dark-background;
color: $light-background;
background-color: inherit;
color: inherit;
}
[type="checkbox"]:checked + .theme-app .crescent{

View File

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

View File

@@ -44,7 +44,6 @@ export class UserMfaComponent implements OnInit, OnDestroy {
public getOTP(): void {
this.mgmtUserService.getUserMfas(this.user.id).then(mfas => {
console.log(mfas.toObject().mfasList);
this.dataSource = new MatTableDataSource(mfas.toObject().mfasList);
this.dataSource.sort = this.sort;
}).catch(error => {

View File

@@ -1,16 +1,16 @@
import { Pipe, PipeTransform } from '@angular/core';
import { Observable } from 'rxjs';
import { AuthUserService } from '../services/auth-user.service';
import { AuthService } from '../services/auth.service';
@Pipe({
name: 'hasRole',
})
export class HasRolePipe implements PipeTransform {
constructor(private authUserService: AuthUserService) { }
constructor(private authService: AuthService) { }
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 { Empty } from 'google-protobuf/google/protobuf/empty_pb';
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 {
@@ -11,6 +9,7 @@ import {
Gender,
MfaOtpResponse,
MultiFactors,
MyPermissions,
MyProjectOrgSearchQuery,
MyProjectOrgSearchRequest,
MyProjectOrgSearchResponse,
@@ -38,8 +37,6 @@ import { GrpcService, RequestFactory, ResponseMapper } from './grpc.service';
providedIn: 'root',
})
export class AuthUserService {
private _roleCache: string[] = [];
constructor(private readonly grpcClient: GrpcService,
private grpcBackendService: GrpcBackendService,
) { }
@@ -75,7 +72,6 @@ export class AuthUserService {
);
}
public async GetMyUser(): Promise<UserView> {
return await this.request(
c => c.getMyUser,
@@ -175,7 +171,7 @@ export class AuthUserService {
);
}
private async getMyzitadelPermissions(): Promise<any> {
public async GetMyzitadelPermissions(): Promise<MyPermissions> {
return await this.request(
c => c.getMyZitadelPermissions,
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> {
// return this.grpcClient.auth.getMyUserPhone(new Empty());
return await this.request(
@@ -312,31 +295,4 @@ export class AuthUserService {
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 { AuthConfig, OAuthService } from 'angular-oauth2-oidc';
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 { AuthUserService } from './auth-user.service';
@@ -22,6 +22,8 @@ export class AuthService {
boolean
> = new BehaviorSubject(this.authenticated);
private zitadelPermissions: BehaviorSubject<string[]> = new BehaviorSubject(['user.resourceowner']);
constructor(
private grpcService: GrpcService,
private config: AuthConfig,
@@ -44,9 +46,50 @@ export class AuthService {
).pipe(
take(1),
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 {

View File

@@ -1,8 +1,8 @@
@import './styles/card';
@import 'src/app/modules/card/card';
@import './styles/table';
@import './styles/sidenav-list';
@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 './styles/meta';
@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);
$accent-color: mat-color($accent, 500);
$primary-dark: mat-color($primary, A900);
$inverse-color: mat-color($primary, A600);
$sec-dark: mat-color($primary, A800);
.mat-menu-item {
&.show-all {
height: 2rem;
@@ -30,6 +30,15 @@
color: $primary-color !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 {