fix(console): cleanup user detail and member components, user/me redirect, permission guards, filter, org policy guard, user table, scss cleanup (#808)

* fix: remove user.write guard for filtering

* border color

* fix user routing from member tables

* idp detail layout

* generic contact component

* fix redirect to auth user, user grant disable

* disable policy action without permission, i18n

* user-create flex fix, contact ng-content

* rm unused styles

* sidenav divider

* lint

* chore(deps-dev): bump @angular/cli from 10.1.3 to 10.1.4 in /console (#806)

* fix: user session with external login (#797)

* fix: user session with external login

* fix: tests

* fix: tests

* fix: change idp config name

* fix(container): stop copying / and instead only copy zitadel (#691)

* chore: stop copying / and instead only copy zitadel

* Update Dockerfile

* Update release.yml

* enable anchors debug

* fix(container): don't copy alpine content into scratch execpt pwd

* chore: remove need step

* merge master

* chore(deps-dev): bump @angular/cli from 10.1.3 to 10.1.4 in /console

Bumps [@angular/cli](https://github.com/angular/angular-cli) from 10.1.3 to 10.1.4.
- [Release notes](https://github.com/angular/angular-cli/releases)
- [Commits](https://github.com/angular/angular-cli/compare/v10.1.3...v10.1.4)

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

Co-authored-by: Fabi <38692350+fgerschwiler@users.noreply.github.com>
Co-authored-by: Florian Forster <florian@caos.ch>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* chore(deps-dev): bump @angular/language-service from 10.1.3 to 10.1.4 in /console (#805)

* fix: user session with external login (#797)

* fix: user session with external login

* fix: tests

* fix: tests

* fix: change idp config name

* fix(container): stop copying / and instead only copy zitadel (#691)

* chore: stop copying / and instead only copy zitadel

* Update Dockerfile

* Update release.yml

* enable anchors debug

* fix(container): don't copy alpine content into scratch execpt pwd

* chore: remove need step

* merge master

* chore(deps-dev): bump @angular/language-service in /console

Bumps [@angular/language-service](https://github.com/angular/angular/tree/HEAD/packages/language-service) from 10.1.3 to 10.1.4.
- [Release notes](https://github.com/angular/angular/releases)
- [Changelog](https://github.com/angular/angular/blob/master/CHANGELOG.md)
- [Commits](https://github.com/angular/angular/commits/10.1.4/packages/language-service)

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

Co-authored-by: Fabi <38692350+fgerschwiler@users.noreply.github.com>
Co-authored-by: Florian Forster <florian@caos.ch>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* chore(deps-dev): bump codelyzer from 6.0.0 to 6.0.1 in /console (#804)

* fix: user session with external login (#797)

* fix: user session with external login

* fix: tests

* fix: tests

* fix: change idp config name

* fix(container): stop copying / and instead only copy zitadel (#691)

* chore: stop copying / and instead only copy zitadel

* Update Dockerfile

* Update release.yml

* enable anchors debug

* fix(container): don't copy alpine content into scratch execpt pwd

* chore: remove need step

* merge master

* chore(deps-dev): bump codelyzer from 6.0.0 to 6.0.1 in /console

Bumps [codelyzer](https://github.com/mgechev/codelyzer) from 6.0.0 to 6.0.1.
- [Release notes](https://github.com/mgechev/codelyzer/releases)
- [Changelog](https://github.com/mgechev/codelyzer/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mgechev/codelyzer/commits/6.0.1)

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

Co-authored-by: Fabi <38692350+fgerschwiler@users.noreply.github.com>
Co-authored-by: Florian Forster <florian@caos.ch>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* chore(deps-dev): bump @angular-devkit/build-angular from 0.1000.8 to 0.1001.4 in /console (#803)

* fix: user session with external login (#797)

* fix: user session with external login

* fix: tests

* fix: tests

* fix: change idp config name

* fix(container): stop copying / and instead only copy zitadel (#691)

* chore: stop copying / and instead only copy zitadel

* Update Dockerfile

* Update release.yml

* enable anchors debug

* fix(container): don't copy alpine content into scratch execpt pwd

* chore: remove need step

* merge master

* chore(deps-dev): bump @angular-devkit/build-angular in /console

Bumps [@angular-devkit/build-angular](https://github.com/angular/angular-cli) from 0.1000.8 to 0.1001.4.
- [Release notes](https://github.com/angular/angular-cli/releases)
- [Commits](https://github.com/angular/angular-cli/commits)

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

Co-authored-by: Fabi <38692350+fgerschwiler@users.noreply.github.com>
Co-authored-by: Florian Forster <florian@caos.ch>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Max Peintner <max@caos.ch>

* chore(deps): bump uuid from 8.3.0 to 8.3.1 in /console (#802)

* fix: user session with external login (#797)

* fix: user session with external login

* fix: tests

* fix: tests

* fix: change idp config name

* fix(container): stop copying / and instead only copy zitadel (#691)

* chore: stop copying / and instead only copy zitadel

* Update Dockerfile

* Update release.yml

* enable anchors debug

* fix(container): don't copy alpine content into scratch execpt pwd

* chore: remove need step

* merge master

* chore(deps): bump uuid from 8.3.0 to 8.3.1 in /console

Bumps [uuid](https://github.com/uuidjs/uuid) from 8.3.0 to 8.3.1.
- [Release notes](https://github.com/uuidjs/uuid/releases)
- [Changelog](https://github.com/uuidjs/uuid/blob/master/CHANGELOG.md)
- [Commits](https://github.com/uuidjs/uuid/compare/v8.3.0...v8.3.1)

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

Co-authored-by: Fabi <38692350+fgerschwiler@users.noreply.github.com>
Co-authored-by: Florian Forster <florian@caos.ch>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* create memberstable as common component

* iam member cleanup

* iam + org m table, user table service user avatar

* toast config

* fix selection emitter

* fix project grant table width

* project grant members refactor

* theme optimizations

* member table col delete

* lint

* fix table row color

* refactor grey color

* lint scss

* org list redirect on click, fix user table undef

* refresh table after grant add

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Fabi <38692350+fgerschwiler@users.noreply.github.com>
Co-authored-by: Florian Forster <florian@caos.ch>
This commit is contained in:
Max Peintner
2020-10-08 10:09:10 +02:00
committed by GitHub
parent 5f0cddac37
commit 0bbc9c7c49
129 changed files with 2002 additions and 2208 deletions

View File

@@ -15,14 +15,15 @@
.content {
width: 100%;
display: flex wrap;
display: flex;
flex-wrap: wrap;
flex-direction: row;
margin: 0 -.5rem;
.section {
padding: .5rem;
flex-basis: 100%;
color: #8795a1;
color: var(--grey);
font-size: .9rem;
}

View File

@@ -15,14 +15,15 @@
.content {
width: 100%;
display: flex wrap;
display: flex;
flex-wrap: wrap;
flex-direction: row;
margin: 0 -.5rem;
.section {
padding: .5rem;
flex-basis: 100%;
color: #8795a1;
color: var(--grey);
font-size: .9rem;
}

View File

@@ -27,18 +27,12 @@
</div>
</app-card>
<!-- <div class="col" *ngIf="user"> -->
<app-card *ngIf="user" class="app-card" title="{{ 'USER.PROFILE.TITLE' | translate }}">
<app-detail-form [genders]="genders" [languages]="languages" [username]="user.userName" [user]="user.human"
(changedLanguage)="changedLanguage($event)" (submitData)="saveProfile($event)">
</app-detail-form>
</app-card>
<!-- <app-card title="Theme" class="app-card theme-card">
<app-theme-setting></app-theme-setting>
</app-card> -->
<!-- </div> -->
<app-card *ngIf="user && user.human && user.id" title="{{ 'USER.EXTERNALIDP.TITLE' | translate }}"
description="{{ 'USER.EXTERNALIDP.DESC' | translate }}">
<app-external-idps [userId]="user.id" [service]="userService"></app-external-idps>
@@ -46,101 +40,10 @@
<app-card *ngIf="user" title="{{ 'USER.LOGINMETHODS.TITLE' | translate }}"
description="{{ 'USER.LOGINMETHODS.DESCRIPTION' | translate }}">
<div class="method-col">
<div class="method-row">
<span class="label">{{ 'USER.PROFILE.PASSWORD' | translate }}</span>
<span>*********</span>
<div>
<a [routerLink]="['password']" mat-icon-button>
<mat-icon class="icon">chevron_right</mat-icon>
</a>
</div>
</div>
<div class="method-row">
<span class="label">{{ 'USER.EMAIL' | translate }}</span>
<ng-container *ngIf="!emailEditState; else emailEdit">
<div class="actions">
<span class="name">{{user?.human?.email}}</span>
<mat-icon class="icon" *ngIf="user?.human?.isEmailVerified" color="primary"
aria-hidden="false" aria-label="verified icon">
check_circle_outline</mat-icon>
<ng-container *ngIf="user?.human?.email && !user?.human?.isEmailVerified">
<mat-icon class="icon" color="warn" aria-hidden="false" aria-label="not verified icon">
highlight_off
</mat-icon>
<a class="verify" matTooltip="{{'USER.LOGINMETHODS.EMAIL.RESEND' | translate}}"
(click)="resendVerification()">{{'USER.LOGINMETHODS.RESENDCODE' | translate}}</a>
</ng-container>
</div>
<div>
<button (click)="emailEditState = true" mat-icon-button>
<mat-icon class="icon">edit</mat-icon>
</button>
</div>
</ng-container>
<ng-template #emailEdit>
<mat-form-field class="name">
<mat-label>{{ 'USER.EMAIL' | translate }}</mat-label>
<input *ngIf="user.human && user.human.email !== undefined && user.human.email !== null"
matInput [(ngModel)]="user.human.email" />
</mat-form-field>
<button (click)="emailEditState = false" mat-icon-button>
<mat-icon class="icon">close</mat-icon>
</button>
<button *ngIf="user.human" [disabled]="!user.human.email" type="button" color="primary"
(click)="saveEmail()" mat-raised-button>{{ 'ACTIONS.SAVE' | translate }}</button>
</ng-template>
</div>
<div class="method-row">
<span class="label">{{ 'USER.PHONE' | translate }}</span>
<ng-container *ngIf="!phoneEditState; else phoneEdit">
<div class="actions">
<span class="name">{{user?.human?.phone}}</span>
<mat-icon class="icon" *ngIf="user?.human?.isPhoneVerified" color="primary"
aria-hidden="false" aria-label="verified icon">
check_circle_outline</mat-icon>
<ng-container *ngIf="user?.human?.phone && !user?.human?.isPhoneVerified">
<mat-icon class="icon" color="warn" aria-hidden="false" aria-label="not verified icon">
highlight_off
</mat-icon>
<a class="verify" matTooltip="{{'USER.LOGINMETHODS.ENTERCODE_DESC' | translate}}"
(click)="enterCode()">{{'USER.LOGINMETHODS.ENTERCODE' | translate}}</a>
<a class="verify" matTooltip="{{'USER.LOGINMETHODS.PHONE.RESEND' | translate}}"
(click)="resendPhoneVerification()">{{'USER.LOGINMETHODS.RESENDCODE' | translate}}</a>
</ng-container>
</div>
<div>
<button (click)="phoneEditState = true" mat-icon-button>
<mat-icon class="icon">edit</mat-icon>
</button>
</div>
</ng-container>
<ng-template #phoneEdit>
<mat-form-field class="name">
<mat-label>{{ 'USER.PHONE' | translate }}</mat-label>
<input *ngIf="user.human && user.human.phone !== undefined && user.human.phone !== null"
matInput [(ngModel)]="user.human.phone" />
</mat-form-field>
<button (click)="phoneEditState = false" mat-icon-button>
<mat-icon class="icon">close</mat-icon>
</button>
<button *ngIf="user.human && user.human.phone" color="warn" (click)="deletePhone()"
mat-icon-button>
<i class="las la-trash"></i>
</button>
<button *ngIf="user.human" [disabled]="!user.human.phone" type="button" color="primary"
(click)="savePhone()" mat-raised-button>{{ 'ACTIONS.SAVE' | translate }}</button>
</ng-template>
</div>
</div>
<app-contact *ngIf="user?.human" [human]="user.human" (savedPhone)="savePhone($event)"
(savedEmail)="saveEmail($event)" (enteredPhoneCode)="enteredPhoneCode($event)"
(deletedPhone)="deletePhone()" (resendEmailVerification)="resendEmailVerification()"
(resendPhoneVerification)="resendPhoneVerification()"></app-contact>
</app-card>
<app-auth-user-mfa *ngIf="user" #mfaComponent></app-auth-user-mfa>
@@ -149,7 +52,7 @@
<div *ngIf="user" class="side" metainfo>
<div class="details">
<div class="row" *ngIf="user?.preferredLoginName">
<span class="first">Preferred Loginname:</span>
<span class="first">{{'USER.PREFERRED_LOGINNAME' | translate}}</span>
<span class="second"><span style="display: block;">{{user.preferredLoginName}}</span></span>
</div>
</div>

View File

@@ -10,7 +10,7 @@
}
.sub {
color: #8795a1;
color: var(--grey);
}
.theme {
@@ -53,59 +53,6 @@
}
}
.method-col {
display: flex;
flex-direction: column;
margin: -.5rem;
.method-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: .5rem;
border-bottom: 1px solid #ffffff20;
flex-wrap: wrap;
.label,
.name {
margin-right: 1rem;
}
.actions {
flex: 1;
display: flex;
justify-content: flex-end;
align-items: center;
flex-direction: column;
}
.label {
font-size: .9rem;
color: #8795a1;
}
.icon {
margin: .5rem;
}
.verify {
text-decoration: none;
font-size: .8rem;
color: #8795a1;
border-radius: .5rem;
cursor: pointer;
word-wrap: none;
white-space: nowrap;
margin-right: 1rem;
&:hover {
color: white;
text-decoration: underline;
}
}
}
}
.col {
display: flex;
flex-wrap: wrap;
@@ -116,14 +63,6 @@
flex: 1;
margin: .5rem;
}
// .theme-card {
// max-width: 300px;
// @media only screen and (max-width: 450px) {
// max-width: none;
// }
// }
}
.side {

View File

@@ -1,5 +1,4 @@
import { Component, OnDestroy } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { TranslateService } from '@ngx-translate/core';
import { Subscription } from 'rxjs';
import { ChangeType } from 'src/app/modules/changes/changes.component';
@@ -7,8 +6,6 @@ import { Gender, UserAddress, UserEmail, UserPhone, UserProfile, UserView } from
import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
import { ToastService } from 'src/app/services/toast.service';
import { CodeDialogComponent } from './code-dialog/code-dialog.component';
@Component({
selector: 'app-auth-user-detail',
templateUrl: './auth-user-detail.component.html',
@@ -22,9 +19,6 @@ export class AuthUserDetailComponent implements OnDestroy {
private subscription: Subscription = new Subscription();
public emailEditState: boolean = false;
public phoneEditState: boolean = false;
public loading: boolean = false;
public copied: string = '';
@@ -36,7 +30,6 @@ export class AuthUserDetailComponent implements OnDestroy {
public translate: TranslateService,
private toast: ToastService,
public userService: GrpcAuthService,
private dialog: MatDialog,
) {
this.loading = true;
this.userService.GetMyUser().then(user => {
@@ -79,50 +72,31 @@ export class AuthUserDetailComponent implements OnDestroy {
}
}
public saveEmail(): void {
this.emailEditState = false;
if (this.user.human) {
this.userService
.SaveMyUserEmail(this.user.human.email).then((data: UserEmail) => {
this.toast.showInfo('USER.TOAST.EMAILSAVED', true);
if (this.user.human) {
this.user.human.email = data.toObject().email;
}
this.emailEditState = false;
}).catch(error => {
this.toast.showError(error);
this.emailEditState = false;
});
}
public saveEmail(email: string): void {
this.userService
.SaveMyUserEmail(email).then((data: UserEmail) => {
this.toast.showInfo('USER.TOAST.EMAILSAVED', true);
if (this.user.human) {
this.user.human.email = data.toObject().email;
}
}).catch(error => {
this.toast.showError(error);
});
}
public enterCode(): void {
if (this.user.human) {
const dialogRef = this.dialog.open(CodeDialogComponent, {
data: {
number: this.user.human.phone,
},
width: '400px',
});
dialogRef.afterClosed().subscribe(code => {
if (code) {
this.userService.VerifyMyUserPhone(code).then(() => {
this.toast.showInfo('USER.TOAST.PHONESAVED', true);
}).catch(error => {
this.toast.showError(error);
});
}
});
}
public enteredPhoneCode(code: string): void {
this.userService.VerifyMyUserPhone(code).then(() => {
this.toast.showInfo('USER.TOAST.PHONESAVED', true);
}).catch(error => {
this.toast.showError(error);
});
}
public changedLanguage(language: string): void {
this.translate.use(language);
}
public resendVerification(): void {
public resendEmailVerification(): void {
this.userService.ResendEmailVerification().then(() => {
this.toast.showInfo('USER.TOAST.EMAILSAVED', true);
}).catch(error => {
@@ -139,32 +113,26 @@ export class AuthUserDetailComponent implements OnDestroy {
}
public deletePhone(): void {
if (this.user.human) {
this.userService.RemoveMyUserPhone().then(() => {
this.toast.showInfo('USER.TOAST.PHONEREMOVED', true);
if (this.user.human) {
this.user.human.phone = '';
}
this.phoneEditState = false;
}).catch(error => {
this.toast.showError(error);
});
}
this.userService.RemoveMyUserPhone().then(() => {
this.toast.showInfo('USER.TOAST.PHONEREMOVED', true);
if (this.user.human) {
this.user.human.phone = '';
}
}).catch(error => {
this.toast.showError(error);
});
}
public savePhone(): void {
this.phoneEditState = false;
public savePhone(phone: string): void {
if (this.user.human) {
this.userService
.SaveMyUserPhone(this.user.human.phone).then((data: UserPhone) => {
.SaveMyUserPhone(phone).then((data: UserPhone) => {
this.toast.showInfo('USER.TOAST.PHONESAVED', true);
if (this.user.human) {
this.user.human.phone = data.toObject().phone;
}
this.phoneEditState = false;
}).catch(error => {
this.toast.showError(error);
this.phoneEditState = false;
});
}
}

View File

@@ -9,6 +9,6 @@
.theme-conent,
.crescent {
background-color: $primary-dark;
transition: background-color .4s cubic-bezier(.645, .045, .355, 1);
transition: background-color .3s cubic-bezier(.645, .045, .355, 1); // cubic-bezier(.645, .045, .355, 1);
}
}

View File

@@ -34,7 +34,7 @@ $light-background: rgb(220, 220, 220);
background: $light-background;
transform: scale(0);
transform-origin: top right;
transition: transform .4s cubic-bezier(.645, .045, .355, 1);
transition: transform .2s cubic-bezier(.645, .045, .355, 1);
}
p {

View File

@@ -0,0 +1,97 @@
<div class="method-col">
<div class="method-row">
<span class="label">{{ 'USER.PROFILE.PASSWORD' | translate }}</span>
<span>*********</span>
<div>
<ng-content select="[phoneAction]"></ng-content>
<a [disabled]="!canWrite" [routerLink]="['password']" mat-icon-button>
<mat-icon class="icon">chevron_right</mat-icon>
</a>
</div>
</div>
<div class="method-row">
<span class="label">{{ 'USER.EMAIL' | translate }}</span>
<ng-container *ngIf="!emailEditState; else emailEdit">
<div class="actions">
<span class="name">{{human?.email}}</span>
<mat-icon class="icon" *ngIf="human?.isEmailVerified" color="primary" aria-hidden="false"
aria-label="verified icon">
check_circle_outline</mat-icon>
<ng-container *ngIf="human?.email && !human?.isEmailVerified">
<mat-icon class="icon" color="warn" aria-hidden="false" aria-label="not verified icon">
highlight_off
</mat-icon>
<a *ngIf="canWrite" class="verify" matTooltip="{{'USER.LOGINMETHODS.EMAIL.RESEND' | translate}}"
(click)="emitEmailVerification()">{{'USER.LOGINMETHODS.RESENDCODE' | translate}}</a>
</ng-container>
</div>
<div>
<button [disabled]="!canWrite" (click)="emailEditState = true" mat-icon-button>
<mat-icon class="icon">edit</mat-icon>
</button>
</div>
</ng-container>
<ng-template #emailEdit>
<mat-form-field class="name">
<mat-label>{{ 'USER.EMAIL' | translate }}</mat-label>
<input *ngIf="human && human.email !== undefined && human.email !== null" matInput
[(ngModel)]="human.email" />
</mat-form-field>
<button (click)="emailEditState = false" mat-icon-button>
<mat-icon class="icon">close</mat-icon>
</button>
<button *ngIf="human" [disabled]="!human.email" type="button" color="primary" (click)="saveEmail()"
mat-raised-button>{{ 'ACTIONS.SAVE' | translate }}</button>
</ng-template>
</div>
<div class="method-row">
<span class="label">{{ 'USER.PHONE' | translate }}</span>
<ng-container *ngIf="!phoneEditState; else phoneEdit">
<div class="actions">
<span class="name">{{human?.phone}}</span>
<mat-icon class="icon" *ngIf="human?.isPhoneVerified" color="primary" aria-hidden="false"
aria-label="verified icon">
check_circle_outline</mat-icon>
<ng-container *ngIf="human?.phone && !human?.isPhoneVerified">
<mat-icon class="icon" matTooltip="not verified" color="warn" aria-hidden="false"
aria-label="not verified icon">
highlight_off
</mat-icon>
<a *ngIf="!disablePhoneCode && !canWrite" class="verify"
matTooltip="{{'USER.LOGINMETHODS.ENTERCODE_DESC' | translate}}"
(click)="enterCode()">{{'USER.LOGINMETHODS.ENTERCODE' | translate}}</a>
<a *ngIf="canWrite" class="verify" matTooltip="{{'USER.LOGINMETHODS.PHONE.RESEND' | translate}}"
(click)="emitPhoneVerification()">{{'USER.LOGINMETHODS.RESENDCODE' | translate}}</a>
</ng-container>
</div>
<div>
<button [disabled]="!canWrite" (click)="phoneEditState = true" mat-icon-button>
<mat-icon class="icon">edit</mat-icon>
</button>
</div>
</ng-container>
<ng-template #phoneEdit>
<mat-form-field class="name">
<mat-label>{{ 'USER.PHONE' | translate }}</mat-label>
<input *ngIf="human && human.phone !== undefined && human.phone !== null" matInput
[(ngModel)]="human.phone" />
</mat-form-field>
<button (click)="phoneEditState = false" mat-icon-button>
<mat-icon class="icon">close</mat-icon>
</button>
<button *ngIf="human && human.phone" color="warn" (click)="emitDeletePhone()" mat-icon-button>
<i class="las la-trash"></i>
</button>
<button *ngIf="human" [disabled]="!human.phone" type="button" color="primary" (click)="savePhone()"
mat-raised-button>{{ 'ACTIONS.SAVE' | translate }}</button>
</ng-template>
</div>
</div>

View File

@@ -0,0 +1,47 @@
.method-col {
display: flex;
flex-direction: column;
margin: -.5rem;
.method-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: .5rem;
border-bottom: 1px solid #ffffff20;
flex-wrap: wrap;
.actions {
flex: 1;
display: flex;
justify-content: flex-end;
align-items: center;
flex-direction: column;
min-width: 150px;
}
.label {
font-size: .9rem;
min-width: 100px;
color: var(--grey);
}
.icon {
margin: .5rem;
}
.verify {
text-decoration: none;
font-size: .8rem;
color: var(--grey);
border-radius: .5rem;
cursor: pointer;
word-wrap: none;
white-space: nowrap;
&:hover {
text-decoration: underline;
}
}
}
}

View File

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

View File

@@ -0,0 +1,70 @@
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { HumanView as AuthHumanView } from 'src/app/proto/generated/auth_pb';
import { HumanView as MgmtHumanView } from 'src/app/proto/generated/management_pb';
import { CodeDialogComponent } from '../auth-user-detail/code-dialog/code-dialog.component';
@Component({
selector: 'app-contact',
templateUrl: './contact.component.html',
styleUrls: ['./contact.component.scss'],
})
export class ContactComponent implements OnInit {
@Input() disablePhoneCode: boolean = false;
@Input() canWrite: boolean = false;
@Input() human!: AuthHumanView.AsObject | MgmtHumanView.AsObject;
@Output() savedPhone: EventEmitter<string> = new EventEmitter();
@Output() savedEmail: EventEmitter<string> = new EventEmitter();
@Output() resendEmailVerification: EventEmitter<void> = new EventEmitter();
@Output() resendPhoneVerification: EventEmitter<void> = new EventEmitter();
@Output() enteredPhoneCode: EventEmitter<string> = new EventEmitter();
@Output() deletedPhone: EventEmitter<void> = new EventEmitter();
public emailEditState: boolean = false;
public phoneEditState: boolean = false;
constructor(private dialog: MatDialog) { }
ngOnInit(): void {
}
savePhone(): void {
this.phoneEditState = false;
this.savedPhone.emit(this.human.phone);
}
emitDeletePhone(): void {
this.phoneEditState = false;
this.deletedPhone.emit();
}
saveEmail(): void {
this.emailEditState = false;
this.savedEmail.emit(this.human.email);
}
emitEmailVerification(): void {
this.resendEmailVerification.emit();
}
emitPhoneVerification(): void {
this.resendPhoneVerification.emit();
}
public enterCode(): void {
if (this.human) {
const dialogRef = this.dialog.open(CodeDialogComponent, {
data: {
number: this.human.phone,
},
width: '400px',
});
dialogRef.afterClosed().subscribe(code => {
if (code) {
this.enteredPhoneCode.emit(code);
}
});
}
}
}

View File

@@ -31,7 +31,7 @@ export class DetailFormComponent implements OnDestroy, OnChanges {
firstName: [{ value: '', disabled: this.disabled }, Validators.required],
lastName: [{ value: '', disabled: this.disabled }, Validators.required],
nickName: [{ value: '', disabled: this.disabled }],
gender: [{ value: 0 }, { disabled: this.disabled }],
gender: [{ value: 0, disabled: this.disabled }],
preferredLanguage: [{ value: '', disabled: this.disabled }],
});
}
@@ -44,7 +44,7 @@ export class DetailFormComponent implements OnDestroy, OnChanges {
firstName: [{ value: '', disabled: this.disabled }, Validators.required],
lastName: [{ value: '', disabled: this.disabled }, Validators.required],
nickName: [{ value: '', disabled: this.disabled }],
gender: [{ value: 0 }, { disabled: this.disabled }],
gender: [{ value: 0, disabled: this.disabled }],
preferredLanguage: [{ value: '', disabled: this.disabled }],
});

View File

@@ -4,7 +4,7 @@
}
.desc {
color: #8795a1;
color: var(--grey);
font-size: .9rem;
}

View File

@@ -33,7 +33,7 @@
}
.left {
color: #8795a1;
color: var(--grey);
margin-right: 1rem;
margin-top: 0;
margin-bottom: .5rem;

View File

@@ -18,7 +18,7 @@
.sub-header {
font-size: .8rem;
color: #8795a1;
color: var(--grey);
}
.people {
@@ -84,7 +84,7 @@
box-sizing: border-box;
font-size: 8px;
border-radius: .5rem;
transition: background-color .2s ease-in-out;
transition: background-color .3s cubic-bezier(.645, .045, .355, 1);
background-color: $accent-color;
cursor: pointer;
flex-direction: column;

View File

@@ -2,6 +2,7 @@ import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { AuthGuard } from 'src/app/guards/auth.guard';
import { RoleGuard } from 'src/app/guards/role.guard';
import { UserGuard } from 'src/app/guards/user.guard';
import { AuthUserDetailComponent } from './auth-user-detail/auth-user-detail.component';
import { PasswordComponent } from './password/password.component';
@@ -41,7 +42,7 @@ const routes: Routes = [
{
path: ':id',
component: UserDetailComponent,
canActivate: [AuthGuard, RoleGuard],
canActivate: [AuthGuard, UserGuard, RoleGuard],
data: {
roles: ['user.read'],
animation: 'HomePage',

View File

@@ -46,6 +46,7 @@ import { PasswordComponent } from './password/password.component';
import { UserDetailRoutingModule } from './user-detail-routing.module';
import { UserDetailComponent } from './user-detail/user-detail.component';
import { UserMfaComponent } from './user-detail/user-mfa/user-mfa.component';
import { ContactComponent } from './contact/contact.component';
@NgModule({
declarations: [
@@ -60,6 +61,7 @@ import { UserMfaComponent } from './user-detail/user-mfa/user-mfa.component';
MembershipsComponent,
MachineKeysComponent,
ExternalIdpsComponent,
ContactComponent,
],
imports: [
UserDetailRoutingModule,

View File

@@ -68,129 +68,34 @@
<app-card *ngIf="user.human" title="{{ 'USER.LOGINMETHODS.TITLE' | translate }}"
description="{{ 'USER.LOGINMETHODS.DESCRIPTION' | translate }}">
<div class="method-col">
<div class="method-row">
<span class="label">{{ 'USER.PROFILE.PASSWORD' | translate }}</span>
<span>******</span>
<div>
<button [disabled]="(canWrite$ | async) == false" (click)="sendSetPasswordNotification()"
mat-stroked-button color="primary"
*ngIf="user.state === UserState.USERSTATE_INITIAL">{{ 'USER.PASSWORD.RESENDNOTIFICATION' | translate }}</button>
<a [disabled]="(canWrite$ | async) == false" [routerLink]="['password']" mat-icon-button>
<mat-icon class="icon">chevron_right</mat-icon>
</a>
</div>
</div>
<div class="method-row">
<span class="label">{{ 'USER.EMAIL' | translate }}</span>
<ng-container *ngIf="!emailEditState; else emailEdit">
<div class="actions">
<span class="name">{{user?.human?.email}}</span>
<mat-icon class="icon" *ngIf="user?.human?.isEmailVerified" color="primary"
aria-hidden="false" aria-label="verified icon">
check_circle_outline</mat-icon>
<ng-container *ngIf="user?.human?.email && !user?.human?.isEmailVerified">
<mat-icon class="icon" color="warn" aria-hidden="false" aria-label="not verified icon">
highlight_off
</mat-icon>
<ng-container *ngIf="(canWrite$ | async)">
<a class="verify" matTooltip="{{'USER.LOGINMETHODS.EMAIL.RESEND' | translate}}"
(click)="resendVerification()">{{'USER.LOGINMETHODS.RESENDCODE' | translate}}</a>
</ng-container>
</ng-container>
</div>
<div>
<button [disabled]="(canWrite$ | async) == false" (click)="emailEditState = true"
mat-icon-button>
<mat-icon class="icon">edit</mat-icon>
</button>
</div>
</ng-container>
<ng-template #emailEdit>
<mat-form-field class="name">
<mat-label>{{ 'USER.EMAIL' | translate }}</mat-label>
<input matInput
*ngIf="user.human && user.human.email !== undefined && user.human.email !== null"
[(ngModel)]="user.human.email" />
</mat-form-field>
<button (click)="emailEditState = false" mat-icon-button>
<mat-icon class="icon">close</mat-icon>
</button>
<button *ngIf="user.human" [disabled]="!user.human.email || (canWrite$ | async) == false"
type="button" color="primary" (click)="saveEmail()"
mat-raised-button>{{ 'ACTIONS.SAVE' | translate }}</button>
</ng-template>
</div>
<div class="method-row">
<span class="label">{{ 'USER.PHONE' | translate }}</span>
<ng-container *ngIf="!phoneEditState; else phoneEdit">
<div class="actions">
<span class="name">{{user?.human?.phone}}</span>
<mat-icon class="icon" *ngIf="user?.human?.isPhoneVerified" color="primary"
aria-hidden="false" aria-label="verified icon">
check_circle_outline</mat-icon>
<ng-container *ngIf="user?.human?.phone && !user?.human?.isPhoneVerified">
<mat-icon class="icon" color="warn" aria-hidden="false" aria-label="not verified icon">
highlight_off
</mat-icon>
<ng-container *ngIf="(canWrite$ | async)">
<a class="verify" matTooltip="{{'USER.LOGINMETHODS.PHONE.RESEND' | translate}}"
(click)="resendPhoneVerification()">{{'USER.LOGINMETHODS.RESENDCODE' | translate}}</a>
</ng-container>
</ng-container>
</div>
<div>
<button [disabled]="(canWrite$ | async) == false" (click)="phoneEditState = true"
mat-icon-button>
<mat-icon class="icon">edit</mat-icon>
</button>
</div>
</ng-container>
<ng-template #phoneEdit>
<mat-form-field class="name">
<mat-label>{{ 'USER.PHONE' | translate }}</mat-label>
<input *ngIf="user.human && user.human.phone !== undefined && user.human.phone !== null"
matInput [disabled]="(canWrite$ | async) == false" [(ngModel)]="user.human.phone" />
</mat-form-field>
<button matTooltip="{{ 'ACTIONS.CLOSE' | translate }}" (click)="phoneEditState = false"
mat-icon-button [disabled]="(canWrite$ | async) == false">
<mat-icon class="icon">close</mat-icon>
</button>
<button *ngIf="user.human?.phone" color="warn" (click)="deletePhone()"
[disabled]="(canWrite$ | async) == false" mat-icon-button
matTooltip="{{ 'ACTIONS.CLEAR' | translate }}">
<i class="las la-trash"></i>
</button>
<button *ngIf="user.human" [disabled]="!user.human.phone || (canWrite$ | async) == false"
type="button" color="primary" (click)="savePhone()"
matTooltip="{{ 'ACTIONS.SAVE' | translate }}" mat-raised-button>
{{ 'ACTIONS.SAVE' | translate }}</button>
</ng-template>
</div>
</div>
<app-contact disablePhoneCode="true"
[canWrite]="(['user.write:' + user?.id, 'user.write'] | hasRole | async)" *ngIf="user?.human"
[human]="user.human" (savedPhone)="savePhone($event)" (savedEmail)="saveEmail($event)"
(deletedPhone)="deletePhone()" (resendEmailVerification)="resendEmailVerification()"
(resendPhoneVerification)="resendPhoneVerification()">
<button phoneAction [disabled]="(canWrite$ | async) == false" (click)="sendSetPasswordNotification()"
mat-stroked-button color="primary"
*ngIf="user.state === UserState.USERSTATE_INITIAL">{{ 'USER.PASSWORD.RESENDNOTIFICATION' | translate }}</button>
</app-contact>
</app-card>
<app-user-mfa *ngIf="user && user.human" [user]="user"></app-user-mfa>
<app-card *ngIf="user?.id" title="{{ 'GRANTS.USER.TITLE' | translate }}"
description="{{'GRANTS.USER.DESCRIPTION' | translate }}">
<app-user-grants [userId]="user.id" [allowCreate]="['user.grant.write'] | hasRole | async"
<app-user-grants [userId]="user.id"
[allowWrite]="['user.grant.write$'+ 'user.grant.write:'+user?.id] | hasRole | async"
[displayedColumns]="['select', 'projectId', 'creationDate', 'changeDate', 'roleNamesList']"
[allowDelete]="['user.grant.delete'] | hasRole | async"></app-user-grants>
[allowDelete]="['user.grant.delete$', 'user.grant.delete'+ user?.id] | hasRole | async">
</app-user-grants>
</app-card>
</div>
<div *ngIf="user" class="side" metainfo>
<div class="details">
<div class="row" *ngIf="user?.preferredLoginName">
<span class="first">Preferred Loginname:</span>
<span class="first">{{'USER.PREFERRED_LOGINNAME' | translate}}</span>
<span class="second"><span style="display: block;">{{user.preferredLoginName}}</span></span>
</div>
</div>

View File

@@ -23,69 +23,6 @@
}
}
.method-col {
display: flex;
flex-direction: column;
margin: -.5rem;
.method-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: .5rem;
border-bottom: 1px solid #ffffff20;
flex-wrap: wrap;
.label,
.name {
padding-right: 1rem;
}
.actions {
flex: 1;
display: flex;
justify-content: flex-end;
align-items: center;
flex-direction: column;
}
.label {
font-size: .9rem;
color: #818a8a;
}
.icon {
margin: .5rem;
}
.verify {
text-decoration: none;
font-size: .8rem;
color: #8795a1;
border-radius: .5rem;
cursor: pointer;
word-wrap: none;
white-space: nowrap;
margin-right: 1rem;
&:hover {
color: white;
text-decoration: underline;
}
}
@media only screen and (max-width: 700px) {
flex-direction: column;
align-items: center;
.label,
.name {
padding-right: 0;
}
}
}
}
.img-phone-email {
width: 300px;
}

View File

@@ -31,8 +31,6 @@ export class UserDetailComponent implements OnInit, OnDestroy {
public languages: string[] = ['de', 'en'];
private subscription: Subscription = new Subscription();
public emailEditState: boolean = false;
public phoneEditState: boolean = false;
public ChangeType: any = ChangeType;
public loading: boolean = false;
@@ -127,7 +125,7 @@ export class UserDetailComponent implements OnInit, OnDestroy {
}
}
public resendVerification(): void {
public resendEmailVerification(): void {
this.mgmtUserService.ResendEmailVerification(this.user.id).then(() => {
this.toast.showInfo('USER.TOAST.EMAILVERIFICATIONSENT', true);
}).catch(error => {
@@ -136,6 +134,7 @@ export class UserDetailComponent implements OnInit, OnDestroy {
}
public resendPhoneVerification(): void {
console.log('resend phone ver', this.user.id);
this.mgmtUserService.ResendPhoneVerification(this.user.id).then(() => {
this.toast.showInfo('USER.TOAST.PHONEVERIFICATIONSENT', true);
}).catch(error => {
@@ -149,37 +148,32 @@ export class UserDetailComponent implements OnInit, OnDestroy {
if (this.user.human) {
this.user.human.phone = '';
}
this.phoneEditState = false;
}).catch(error => {
this.toast.showError(error);
});
}
public saveEmail(): void {
this.emailEditState = false;
if (this.user && this.user.human?.email) {
this.mgmtUserService
.SaveUserEmail(this.user.id, this.user.human.email).then((data: UserEmail) => {
this.toast.showInfo('USER.TOAST.EMAILSENT', true);
if (this.user.human) {
this.user.human.email = data.toObject().email;
}
}).catch(error => {
this.toast.showError(error);
});
public saveEmail(email: string): void {
if (this.user.id && email) {
this.mgmtUserService.SaveUserEmail(this.user.id, email).then((data: UserEmail) => {
this.toast.showInfo('USER.TOAST.EMAILSENT', true);
if (this.user.human) {
this.user.human.email = data.toObject().email;
}
}).catch(error => {
this.toast.showError(error);
});
}
}
public savePhone(): void {
this.phoneEditState = false;
if (this.user && this.user.human?.phone) {
public savePhone(phone: string): void {
if (this.user.id && phone) {
this.mgmtUserService
.SaveUserPhone(this.user.id, this.user.human.phone).then((data: UserPhone) => {
.SaveUserPhone(this.user.id, phone).then((data: UserPhone) => {
this.toast.showInfo('USER.TOAST.PHONESAVED', true);
if (this.user.human) {
this.user.human.phone = data.toObject().phone;
}
this.phoneEditState = false;
}).catch(error => {
this.toast.showError(error);
});

View File

@@ -3,6 +3,6 @@ h1 {
}
.sub {
color: #8795a1;
color: var(--grey);
margin-bottom: 2rem;
}

View File

@@ -1,13 +1,13 @@
<app-refresh-table [loading]="loading$ | async" (refreshed)="refreshPage()" [dataSize]="dataSource.data.length"
[timestamp]="userResult?.viewTimestamp" [selection]="selection"
[emitRefreshOnPreviousRoute]="refreshOnPreviousRoute">
<ng-template appHasRole [appHasRole]="['user.write']" actions>
<mat-form-field @appearfade *ngIf="userSearchKey != undefined" actions class="filtername">
<mat-label>{{'USER.PAGES.FILTER' | translate}}</mat-label>
<input matInput (keyup)="applyFilter($event)"
[placeholder]="('USER.TABLE.FILTER.' + userSearchKey.toString()) | translate" #input>
</mat-form-field>
<mat-form-field @appearfade *ngIf="userSearchKey != undefined" actions class="filtername">
<mat-label>{{'USER.PAGES.FILTER' | translate}}</mat-label>
<input matInput (keyup)="applyFilter($event)"
[placeholder]="('USER.TABLE.FILTER.' + userSearchKey.toString()) | translate" #input>
</mat-form-field>
<ng-template appHasRole [appHasRole]="['user.write']" actions>
<button (click)="deactivateSelectedUsers()" matTooltip="{{'USER.TABLE.DEACTIVATE' | translate}}"
class="icon-button" mat-icon-button *ngIf="selection.hasValue()" [disabled]="disabled">
<mat-icon svgIcon="mdi_account_cancel"></mat-icon>
@@ -25,18 +25,24 @@
<div class="table-wrapper">
<table class="table" mat-table [dataSource]="dataSource">
<ng-container matColumnDef="select">
<th mat-header-cell *matHeaderCellDef>
<th mat-header-cell *matHeaderCellDef class="selection">
<mat-checkbox color="primary" (change)="$event ? masterToggle() : null"
[checked]="selection.hasValue() && isAllSelected()"
[indeterminate]="selection.hasValue() && !isAllSelected()">
</mat-checkbox>
</th>
<td mat-cell *matCellDef="let user">
<td mat-cell *matCellDef="let user" class="selection">
<mat-checkbox color="primary" (click)="$event.stopPropagation()"
(change)="$event ? selection.toggle(user) : null" [checked]="selection.isSelected(user)">
<app-avatar *ngIf="user[userType] && user[userType].displayName" class="avatar"
[name]="user[userType].displayName" [size]="32">
<app-avatar
*ngIf="user[userType] && user[userType].displayName && user[userType]?.firstName && user[userType]?.lastName; else cog"
class="avatar" [name]="user[userType].displayName" [size]="32">
</app-avatar>
<ng-template #cog>
<div class="sa-icon">
<i class="las la-user-cog"></i>
</div>
</ng-template>
</mat-checkbox>
</td>
</ng-container>

View File

@@ -1,4 +1,3 @@
.table-wrapper {
overflow: auto;
@@ -8,7 +7,7 @@
td,
th {
padding: 0 1rem;
padding: .5rem;
&:first-child {
padding-left: 0;
@@ -19,6 +18,11 @@
padding-right: 0;
}
}
.selection {
width: 50px;
max-width: 50px;
}
}
}
@@ -52,3 +56,13 @@ th {
.filtername {
margin: 0 1rem;
}
.sa-icon {
display: block;
width: 32px;
margin: 0 .5rem;
i {
margin: auto;
}
}

View File

@@ -1,6 +1,7 @@
import { SelectionModel } from '@angular/cdk/collections';
import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { MatInput } from '@angular/material/input';
import { MatPaginator, PageEvent } from '@angular/material/paginator';
import { MatTableDataSource } from '@angular/material/table';
import { TranslateService } from '@ngx-translate/core';
@@ -29,6 +30,7 @@ export class UserTableComponent implements OnInit {
@Input() refreshOnPreviousRoute: string = '';
@Input() disabled: boolean = false;
@ViewChild(MatPaginator) public paginator!: MatPaginator;
@ViewChild('input') public filter!: MatInput;
public dataSource: MatTableDataSource<UserView.AsObject> = new MatTableDataSource<UserView.AsObject>();
public selection: SelectionModel<UserView.AsObject> = new SelectionModel<UserView.AsObject>(true, []);
public userResult!: UserSearchResponse.AsObject;
@@ -38,6 +40,7 @@ export class UserTableComponent implements OnInit {
@Output() public changedSelection: EventEmitter<Array<UserView.AsObject>> = new EventEmitter();
UserSearchKey: any = UserSearchKey;
constructor(
public translate: TranslateService,
private userService: ManagementService,
@@ -119,6 +122,7 @@ export class UserTableComponent implements OnInit {
public applyFilter(event: Event): void {
const filterValue = (event.target as HTMLInputElement).value;
this.getData(
this.paginator.pageSize,
this.paginator.pageIndex * this.paginator.pageSize,
@@ -128,6 +132,12 @@ export class UserTableComponent implements OnInit {
}
public setFilter(key: UserSearchKey): void {
setTimeout(() => {
if (this.filter) {
(this.filter as any).nativeElement.focus();
}
}, 100);
if (this.userSearchKey !== key) {
this.userSearchKey = key;
} else {