fix(console): remove cropper, set avatar url if available (#1865)

* set avatarurl if available

* lint

* force sidemargin

* dont load image via asset

* rm log

* stylelint

* add ZITADEL domain to csp img src

* sanitize url

* fix undefined link projects

* use name as fallback

* operator: rename uploadServiceURL to assetServiceURL in environment json for console

* remove data

* rm logs

* center crop image

* add avatar to changes

Co-authored-by: Livio Amstutz <livio.a@gmail.com>
This commit is contained in:
Max Peintner
2021-06-14 13:53:40 +02:00
committed by GitHub
parent ab78b34c6c
commit 465081ee6d
39 changed files with 111 additions and 153 deletions

View File

@@ -60,7 +60,7 @@
<div (clickOutside)="closeAccountCard()" class="icon-container">
<app-avatar
*ngIf="user && (user.human?.profile?.displayName || (user.human?.profile?.firstName && user.human?.profile?.lastName))"
class="avatar dontcloseonclick" (click)="showAccount = !showAccount" [active]="showAccount" [forColor]="user?.preferredLoginName"
class="avatar dontcloseonclick" (click)="showAccount = !showAccount" [active]="showAccount" [avatarUrl]="user.human?.profile?.avatarUrl || ''" [forColor]="user?.preferredLoginName"
[name]="user.human.profile.displayName ? user.human.profile.displayName : (user.human.profile.firstName + ' '+ user.human.profile.lastName)"
[size]="38">
</app-avatar>

View File

@@ -2,7 +2,7 @@
<app-avatar
*ngIf="user.human?.profile && (user.human?.profile?.displayName || (user.human?.profile?.firstName && user.human?.profile?.lastName))"
class="avatar"
[forColor]="user.preferredLoginName"
[forColor]="user.preferredLoginName" [avatarUrl]="user.human?.profile?.avatarUrl || ''"
[name]="user.human?.profile?.displayName ? user.human?.profile?.displayName : (user.human?.profile?.firstName + ' '+ user.human?.profile?.lastName)"
[size]="80">
</app-avatar>
@@ -15,7 +15,7 @@
<div class="l-accounts">
<mat-progress-bar *ngIf="loadingUsers" color="primary" mode="indeterminate"></mat-progress-bar>
<a class="row" *ngFor="let session of sessions" (click)="selectAccount(session.loginName)">
<app-avatar *ngIf="session && session.displayName" class="small-avatar" [forColor]="session.loginName" [size]="32">
<app-avatar *ngIf="session && session.displayName" class="small-avatar" [avatarUrl]="session.avatarUrl || ''" [forColor]="session.loginName" [size]="32">
</app-avatar>
<div class="col">

View File

@@ -20,7 +20,6 @@ export class AccountsCardComponent implements OnInit {
constructor(public authService: AuthenticationService, private router: Router, private userService: GrpcAuthService) {
this.userService.listMyUserSessions().then(sessions => {
this.sessions = sessions.resultList;
console.log(sessions.resultList);
const index = this.sessions.findIndex(user => user.loginName === this.user.preferredLoginName);
if (index > -1) {
this.sessions.splice(index, 1);

View File

@@ -2,5 +2,6 @@
matRippleCentered="true"
[ngStyle]="{'height': size+'px', 'width': size+'px', 'fontSize': (fontSize-1)+'px', 'background': color}"
[ngClass]="{'active': active}">
{{credentials}}
<img class="dontcloseonclick" *ngIf="avatarUrl; else creds" [src]="avatarUrl"/>
<ng-template #creds>{{credentials}}</ng-template>
</div>

View File

@@ -17,5 +17,13 @@
outline: none;
color: white;
font-weight: bold;
img {
width: 100%;
height: 100%;
border-radius: 50%;
object-fit: cover;
object-position: center;
}
}
}

View File

@@ -13,6 +13,7 @@ export class AvatarComponent implements OnInit {
@Input() active: boolean = false;
@Input() color: string = '';
@Input() forColor: string = '';
@Input() avatarUrl: string = '';
constructor() { }
ngOnInit(): void {
@@ -21,6 +22,11 @@ export class AvatarComponent implements OnInit {
if (!this.color) {
this.color = this.getColor(this.forColor || '');
}
} else if (!this.credentials && this.name) {
this.credentials = this.getInitials(this.name);
if (!this.color) {
this.color = this.getColor(this.name || '');
}
}
if (this.size > 50) {

View File

@@ -13,7 +13,8 @@
<div class="row">
<app-avatar matTooltip="{{ dayelement.editorDisplayName }}"
*ngIf="dayelement.editorDisplayName; else spacer" class="avatar"
[name]="dayelement.editorDisplayName" [size]="32" [forColor]="dayelement?.preferredLoginName">
[name]="dayelement.editorDisplayName" [size]="32" [forColor]="dayelement?.editorPreferredLoginName"
[avatarUrl]="dayelement.editorAvatarUrl || ''">
</app-avatar>
<ng-template #spacer>
<div class="spacer"></div>
@@ -40,4 +41,4 @@
<mat-spinner *ngIf="loading | async" diameter="25"></mat-spinner>
</div>
<span class="end-container" *ngIf="bottom">{{'CHANGES.BOTTOM' | translate}}</span>
</div>
</div>

View File

@@ -190,6 +190,8 @@ export class ChangesComponent implements OnInit, OnDestroy {
editor: change.editorDisplayName,
editorId: change.editorId,
editorDisplayName: change.editorDisplayName,
editorPreferredLoginName: change.editorPreferredLoginName,
editorAvatarUrl: change.editorAvatarUrl,
dates: [change.changeDate],
// data: [change.data],
@@ -211,6 +213,8 @@ export class ChangesComponent implements OnInit, OnDestroy {
editor: change.editorDisplayName,
editorId: change.editorId,
editorDisplayName: change.editorDisplayName,
editorPreferredLoginName: change.editorPreferredLoginName,
editorAvatarUrl: change.editorAvatarUrl,
dates: [change.changeDate],
// data: [change.data],

View File

@@ -14,7 +14,7 @@
[ngStyle]="{'z-index': 100 - i}">
<app-avatar
*ngIf="member && member.displayName && member.firstName && member.lastName; else cog"
class="avatar dontcloseonclick" [forColor]="member?.userName"[forColor]="member?.preferredLoginName"
class="avatar dontcloseonclick" [avatarUrl]="member.avatarUrl|| ''" [forColor]="member?.userName"[forColor]="member?.preferredLoginName"
[name]="member.displayName ? member.displayName : (member.firstName + ' '+ member.lastName)"
[size]="32">
</app-avatar>

View File

@@ -22,7 +22,7 @@
<mat-checkbox [disabled]="!canWrite" color="primary" (click)="$event.stopPropagation()"
(change)="$event ? selection.toggle(row) : null" [checked]="selection.isSelected(row)">
<app-avatar *ngIf="row?.displayName && row.firstName && row.lastName; else cog" class="avatar"
[name]="row.displayName" [forColor]="row?.preferredLoginName" [size]="32">
[name]="row.displayName" [avatarUrl]="row.avatarUrl || ''" [avatarUrl]="row.avatarUrl|| ''" [forColor]="row?.preferredLoginName" [size]="32">
</app-avatar>
<ng-template #cog>
<div class="sa-icon">

View File

@@ -482,6 +482,7 @@ export class PrivateLabelingPolicyComponent implements OnDestroy {
private loadAsset(imagekey: string, url: string): Promise<any> {
return this.assetService.load(`${url}`, this.org.id).then(data => {
console.log(data);
const objectURL = URL.createObjectURL(data);
this.images[imagekey] = this.sanitizer.bypassSecurityTrustUrl(objectURL);
this.refreshPreview.emit();

View File

@@ -57,7 +57,7 @@
<div class="circle">
<app-avatar
*ngIf="user.human && user.human.displayName && user.human?.firstName && user.human?.lastName; else cog"
class="avatar" [name]="user.human.displayName" [forColor]="user?.preferredLoginName" [size]="32">
class="avatar" [name]="user.human.displayName" [avatarUrl]="user.human?.profile?.avatarUrl || ''" [forColor]="user?.preferredLoginName" [size]="32">
</app-avatar>
<ng-template #cog>
<div class="sa-icon">

View File

@@ -31,7 +31,7 @@
(change)="$event ? selection.toggle(row) : null" [checked]="selection.isSelected(row)">
<app-avatar
*ngIf="context !== UserGrantContext.USER && row && row?.displayName && row.firstName && row.lastName"
class="avatar" [name]="row.displayName" [forColor]="row?.preferredLoginName" [size]="32">
class="avatar" [name]="row.displayName" [avatarUrl]="row.avatarUrl || ''" [forColor]="row?.preferredLoginName" [size]="32">
</app-avatar>
</mat-checkbox>
</td>

View File

@@ -3,6 +3,7 @@
<app-avatar [routerLink]="['/users/me']"
*ngIf="user && (user.human?.profile?.displayName || (user.human?.profile?.firstName && user.human?.profile?.lastName))"
class="avatar"
[avatarUrl]="user.human?.profile?.avatarUrl || ''"
[forColor]="user?.preferredLoginName"
[name]="user.human?.profile?.displayName ? user.human?.profile?.displayName : (user.human?.profile?.firstName + ' '+ user.human?.profile?.lastName)"
[size]="100">

View File

@@ -67,7 +67,7 @@
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr class="highlight" mat-row *matRowDef="let row; columns: displayedColumns;"
[routerLink]="['/granted-projects', row.projectId, 'grant', row.id]"></tr>
[routerLink]="['/granted-projects', row.projectId, 'grant', row.grantId]"></tr>
</table>
<div *ngIf="(loading$ | async) == false && !dataSource?.data?.length" class="no-content-row">

View File

@@ -66,9 +66,9 @@
<ng-container matColumnDef="actions" stickyEnd>
<th mat-header-cell *matHeaderCellDef></th>
<td mat-cell *matCellDef="let project">
<button class="dlt-button" *ngIf="project.projectId !== zitadelProjectId" color="warn"
<button class="dlt-button" *ngIf="project.id !== zitadelProjectId" color="warn"
mat-icon-button matTooltip="{{'ACTIONS.DELETE' | translate}}"
(click)="deleteProject(project.projectId)">
(click)="deleteProject(project.id)">
<i class="las la-trash"></i>
</button>
</td>
@@ -76,7 +76,7 @@
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr class="highlight" mat-row *matRowDef="let row; columns: displayedColumns;"
[routerLink]="['/projects', row.projectId]"></tr>
[routerLink]="['/projects', row.id]"></tr>
</table>
<div *ngIf="(loading$ | async) == false && !dataSource?.data?.length" class="no-content-row">

View File

@@ -5,10 +5,9 @@
<div class="i-wrapper" *ngIf="showEditImage">
<i class="las la-camera"></i>
</div>
<img class="pic" [src]="profilePic" *ngIf="profilePic"/>
<app-avatar
*ngIf="!profilePic && user && user.profile?.displayName && user.profile?.firstName && user?.profile.lastName"
class="avatar" [name]="user.profile?.displayName" [forColor]="preferredLoginName" [size]="80">
*ngIf="user && user.profile?.displayName && user.profile?.firstName && user?.profile.lastName"
class="avatar" [name]="user.profile?.displayName" [avatarUrl]="user?.profile?.avatarUrl || ''" [forColor]="preferredLoginName" [size]="80">
</app-avatar>
</button>

View File

@@ -46,13 +46,6 @@
background-color: #00000080;
}
}
.pic {
height: 80px;
width: 80px;
object-fit: contain;
border-radius: 50%;
}
}
.formfield {

View File

@@ -1,11 +1,8 @@
import { Component, EventEmitter, Input, OnChanges, OnDestroy, Output } from '@angular/core';
import { AbstractControl, FormBuilder, FormGroup, Validators } from '@angular/forms';
import { MatDialog } from '@angular/material/dialog';
import { DomSanitizer } from '@angular/platform-browser';
import { Subscription } from 'rxjs';
import { Gender, Human, User } from 'src/app/proto/generated/zitadel/user_pb';
import { AssetService } from 'src/app/services/asset.service';
import { ToastService } from 'src/app/services/toast.service';
import { ProfilePictureComponent } from './profile-picture/profile-picture.component';
@@ -25,7 +22,6 @@ export class DetailFormComponent implements OnDestroy, OnChanges {
@Output() public submitData: EventEmitter<User> = new EventEmitter<User>();
@Output() public changedLanguage: EventEmitter<string> = new EventEmitter<string>();
public profilePic: any = null;
public profileForm!: FormGroup;
private sub: Subscription = new Subscription();
@@ -33,9 +29,6 @@ export class DetailFormComponent implements OnDestroy, OnChanges {
constructor(
private fb: FormBuilder,
private dialog: MatDialog,
private assetService: AssetService,
private toast: ToastService,
private sanitizer: DomSanitizer,
) {
this.profileForm = this.fb.group({
userName: [{ value: '', disabled: true }, [
@@ -48,8 +41,6 @@ export class DetailFormComponent implements OnDestroy, OnChanges {
gender: [{ value: 0, disabled: this.disabled }],
preferredLanguage: [{ value: '', disabled: this.disabled }],
});
this.loadAvatar();
}
public ngOnChanges(): void {
@@ -85,7 +76,7 @@ export class DetailFormComponent implements OnDestroy, OnChanges {
public openUploadDialog(): void {
const dialogRef = this.dialog.open(ProfilePictureComponent, {
data: {
profilePic: this.profilePic,
profilePic: this.user.profile?.avatarUrl,
},
width: '400px',
});
@@ -96,15 +87,6 @@ export class DetailFormComponent implements OnDestroy, OnChanges {
});
}
public loadAvatar(): Promise<any> {
return this.assetService.load(`users/me/avatar`).then(data => {
const objectURL = URL.createObjectURL(data);
this.profilePic = this.sanitizer.bypassSecurityTrustUrl(objectURL);
}).catch(error => {
this.toast.showError(error);
});
}
public get userName(): AbstractControl | null {
return this.profileForm.get('userName');
}

View File

@@ -2,38 +2,15 @@
<div mat-dialog-content>
<p class="desc">{{'USER.PROFILE.AVATAR.CURRENT' | translate}}</p>
<div class="current-pic-wrapper">
<img class="pic" [src]="data.profilePic" *ngIf="data.profilePic"/>
<span class="fill-space"></span>
<input #selectedFile style="display: none;" class="file-input" type="file" (change)="onDrop($event)">
<button class="btn" mat-stroked-button type="button" (click)="selectedFile.click();">{{'USER.PROFILE.AVATAR.UPLOADBTN' | translate}}</button>
<button *ngIf="data.profilePic" matTooltip="{{'ACTIONS.DELETE' | translate}}" color="warn" (click)="deletePic()" mat-icon-button><mat-icon>remove_circle</mat-icon></button>
</div>
<ng-container *ngIf="imageChangedEvent">
<p class="desc">{{'USER.PROFILE.AVATAR.PREVIEW' | translate}}</p>
<div class="cropped-preview" *ngIf="croppedImage">
<img class="pic" [src]="croppedImage"/>
<button color="primary" mat-raised-button (click)="upload()">{{'USER.PROFILE.AVATAR.UPLOAD' | translate}}</button>
</div>
<p class="error" *ngIf="showCropperError">{{'USER.PROFILE.AVATAR.CROPPERERROR' | translate}}</p>
<image-cropper
class="cropper"
[imageChangedEvent]="imageChangedEvent"
[maintainAspectRatio]="true"
[aspectRatio]="4 / 4"
[roundCropper]="true"
[autoCrop]="true"
(imageCropped)="imageCropped($event)"
(loadImageFailed)="loadImageFailed()"
></image-cropper>
</ng-container>
<img class="pic" [src]="data.profilePic" *ngIf="data.profilePic"/>
<span class="fill-space"></span>
<input #selectedFile style="display: none;" class="file-input" type="file" (change)="onDrop($event)">
<button class="btn" mat-raised-button color="primary" type="button" (click)="selectedFile.click();">{{'USER.PROFILE.AVATAR.UPLOADBTN' | translate}}</button>
<button *ngIf="data.profilePic" matTooltip="{{'ACTIONS.DELETE' | translate}}" color="warn" (click)="deletePic()" mat-icon-button><mat-icon>remove_circle</mat-icon></button>
</div>
</div>
<div mat-dialog-actions class="action">
<button color="primary" mat-raised-button class="ok-button" (click)="closeDialog()">
<button color="primary" mat-stroked-button class="ok-button" (click)="closeDialog()">
{{'ACTIONS.CLOSE' | translate}}
</button>
</div>

View File

@@ -22,7 +22,8 @@
.pic {
height: 80px;
width: 80px;
object-fit: contain;
object-fit: cover;
object-position: center;
border-radius: 50%;
background-color: #00000030;
}

View File

@@ -1,6 +1,6 @@
import { Component, Inject, OnInit } from '@angular/core';
import { Component, Inject } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { ImageCroppedEvent } from 'ngx-image-cropper';
import { DomSanitizer } from '@angular/platform-browser';
import { AssetService } from 'src/app/services/asset.service';
import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
import { ToastService } from 'src/app/services/toast.service';
@@ -10,40 +10,29 @@ import { ToastService } from 'src/app/services/toast.service';
templateUrl: './profile-picture.component.html',
styleUrls: ['./profile-picture.component.scss'],
})
export class ProfilePictureComponent implements OnInit {
public isHovering: boolean = false;
public imageChangedEvent: any = '';
public imageChangedFormat: string = '';
public croppedImage: any = '';
public showCropperError: boolean = false;
export class ProfilePictureComponent {
constructor(
private authService: GrpcAuthService,
public dialogRef: MatDialogRef<ProfilePictureComponent>,
@Inject(MAT_DIALOG_DATA) public data: any,
private toast: ToastService,
private assetService: AssetService) { }
public ngOnInit(): void {
}
public toggleHover(isHovering: boolean): void {
this.isHovering = isHovering;
private assetService: AssetService,
private sanitizer: DomSanitizer,
) {
}
public onDrop(event: any): Promise<any> | void {
const filelist: FileList = event.target.files;
console.log(event.target.files);
this.imageChangedEvent = event;
const file = filelist.item(0);
if (file) {
this.imageChangedFormat = file.type;
const formData = new FormData();
formData.append('file', file);
return this.handleUploadPromise(this.assetService.upload('users/me/avatar', formData));
}
}
public deletePic(): void {
console.log('delete');
this.authService.removeMyAvatar().then(() => {
this.toast.showInfo('USER.PROFILE.AVATAR.DELETESUCCESS', true);
this.data.profilePic = null;
@@ -55,45 +44,17 @@ export class ProfilePictureComponent implements OnInit {
private handleUploadPromise(task: Promise<any>): Promise<any> {
return task.then(() => {
this.toast.showInfo('POLICY.TOAST.UPLOADSUCCESS', true);
this.data.profilePic = this.croppedImage;
this.assetService.load('users/me/avatar').then(data => {
const objectURL = URL.createObjectURL(data);
const pic = this.sanitizer.bypassSecurityTrustUrl(objectURL);
this.data.profilePic = pic;
}).catch(error => {
this.toast.showError(error);
});
}).catch(error => this.toast.showError(error));
}
public fileChangeEvent(event: any): void {
this.imageChangedEvent = event;
}
public imageCropped(event: ImageCroppedEvent): void {
this.showCropperError = false;
this.croppedImage = event.base64;
}
public upload(): Promise<any> | void {
const formData = new FormData();
const splitted = this.croppedImage.split(';base64,');
if (splitted[1]) {
const blob = this.base64toBlob(splitted[1]);
formData.append('file', blob);
return this.handleUploadPromise(this.assetService.upload('users/me/avatar', formData));
}
}
public loadImageFailed(): void {
this.showCropperError = true;
}
public closeDialog(): void {
this.dialogRef.close(false);
}
public base64toBlob(b64: string): Blob {
const byteCharacters = atob(b64);
const byteNumbers = new Array(byteCharacters.length);
for (let i = 0; i < byteCharacters.length; i++) {
byteNumbers[i] = byteCharacters.charCodeAt(i);
}
const byteArray = new Uint8Array(byteNumbers);
const blob = new Blob([byteArray], { type: this.imageChangedFormat });
return blob;
}
}

View File

@@ -35,7 +35,7 @@
<app-avatar
*ngIf="user.human && user.human.profile.displayName && user.human?.profile.firstName && user.human?.profile.lastName; else cog"
class="avatar" [name]="user.human.profile.displayName" [forColor]="user?.preferredLoginName" [size]="32">
class="avatar" [name]="user.human.profile.displayName" [avatarUrl]="user.human?.profile?.avatarUrl || ''" [forColor]="user?.preferredLoginName" [size]="32">
</app-avatar>
<ng-template #cog>
<div class="sa-icon" *ngIf="user.machine">

View File

@@ -312,3 +312,7 @@ h2 {
i {
font-size: 1.5rem;
}
.mat-checkbox-inner-container.mat-checkbox-inner-container-no-side-margin {
margin-right: .5rem !important;
}