mirror of
https://github.com/andwati/zitadel.git
synced 2026-11-15 02:15:14 +00:00
feat: rehauled console (#3525)
* new console * move npm ci to angular build * rel path for assets * local grpc copy * login policy, rm clear views, features rel path * lock Co-authored-by: Livio Amstutz <[email protected]>
This commit is contained in:
co-authored by
Livio Amstutz
parent
00f7dbe875
commit
08ae39ae19
+86
-61
@@ -1,75 +1,100 @@
|
||||
<form>
|
||||
<cnsl-form-field *ngIf="target && target === UserTarget.SELF" appearance="outline" class="full-width more-space">
|
||||
<cnsl-label>Organizations User Loginname</cnsl-label>
|
||||
<div class="found" *ngIf="users.length > 0">
|
||||
<!-- <span class="found-label cnsl-secondary-text">{{'USER.SEARCH.FOUND' | translate}}:</span> -->
|
||||
<div class="found-user-row" *ngFor="let user of users; index as i">
|
||||
<div class="circle">
|
||||
<cnsl-avatar
|
||||
*ngIf="user.human && user.human.profile && user.human.profile.displayName && user.human.profile.firstName && user.human.profile.lastName; else cog"
|
||||
class="avatar" [name]="user.human.profile.displayName" [avatarUrl]="user.human.profile?.avatarUrl || ''"
|
||||
[forColor]="user.preferredLoginName" [size]="32">
|
||||
</cnsl-avatar>
|
||||
<ng-template #cog>
|
||||
<cnsl-avatar class="avatar" [forColor]="user.preferredLoginName" [isMachine]="true" [size]="32">
|
||||
</cnsl-avatar>
|
||||
</ng-template>
|
||||
</div>
|
||||
|
||||
<div class="user-name-column" *ngIf="user.human">
|
||||
<span>{{user.human?.profile?.displayName}}</span>
|
||||
<span class="smaller cnsl-secondary-text">{{user.preferredLoginName}}</span>
|
||||
</div>
|
||||
<div class="user-name-column" *ngIf="user.machine">
|
||||
<span>{{user.machine.name}}</span>
|
||||
<span class="smaller cnsl-secondary-text">{{user.preferredLoginName}}</span>
|
||||
</div>
|
||||
<span class="fill-space"></span>
|
||||
<button class="search-user-dl-btn" matTooltip="{{'ACTIONS.REMOVE' | translate}}" mat-icon-button color="warn">
|
||||
<i class="las la-minus-circle" (click)="users.splice(i, 1)"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="add-user-row" *ngIf="!editState">
|
||||
<button (click)="editState = true" mat-icon-button>
|
||||
<mat-icon>add</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form *ngIf="editState">
|
||||
<ng-container *ngIf="target && target === UserTarget.SELF">
|
||||
<div class="line">
|
||||
<cnsl-form-field class="user-create-form-field more-space">
|
||||
<cnsl-label>{{'USER.SEARCH.ADDITIONAL' | translate}}</cnsl-label>
|
||||
|
||||
<input cnslInput *ngIf="singleOutput" type="text" placeholder="Search for the user loginname" #usernameInput
|
||||
[formControl]="myControl" [matAutocomplete]="auto" />
|
||||
[formControl]="myControl" [matAutocomplete]="auto" />
|
||||
|
||||
<mat-chip-list *ngIf="!singleOutput" #chipList aria-label="loginname selection">
|
||||
<mat-chip class="chip" *ngFor="let selecteduser of users" [selectable]="selectable" [removable]="removable"
|
||||
(removed)="remove(selecteduser)">
|
||||
{{ (selecteduser.human && selecteduser.human.profile) ? (selecteduser.human.profile.displayName) :
|
||||
selecteduser?.machine?.name}}
|
||||
| <small>
|
||||
{{selecteduser.preferredLoginName}}</small>
|
||||
<mat-icon matChipRemove *ngIf="removable">cancel</mat-icon>
|
||||
</mat-chip>
|
||||
<input cnslInput placeholder="{{'ORG_DETAIL.MEMBER.LOGINNAME' | translate}}" #usernameInput
|
||||
[formControl]="myControl" [matAutocomplete]="auto" [matChipInputFor]="chipList"
|
||||
[matChipInputSeparatorKeyCodes]="separatorKeysCodes" [matChipInputAddOnBlur]="addOnBlur"
|
||||
(matChipInputTokenEnd)="add($event)" />
|
||||
</mat-chip-list>
|
||||
<input *ngIf="!singleOutput" cnslInput #usernameInput [formControl]="myControl" placeholder="John Doe"
|
||||
[matAutocomplete]="auto" />
|
||||
|
||||
<mat-autocomplete #auto="matAutocomplete" (optionSelected)="selected($event)" [displayWith]="displayFn">
|
||||
<mat-option *ngIf="isLoading" class="is-loading">
|
||||
<mat-spinner diameter="30"></mat-spinner>
|
||||
</mat-option>
|
||||
<mat-option *ngFor="let user of filteredUsers" [value]="user">
|
||||
{{(user.human && user.human.profile && user.human.profile.displayName) ? user.human.profile.displayName : user.machine?.name}}
|
||||
<small>{{user.preferredLoginName}}</small>
|
||||
</mat-option>
|
||||
<mat-option *ngIf="isLoading" class="is-loading">
|
||||
<mat-spinner diameter="30"></mat-spinner>
|
||||
</mat-option>
|
||||
<mat-option *ngFor="let user of filteredUsers" [value]="user">
|
||||
<div class="user-option">
|
||||
<div class="circle">
|
||||
<cnsl-avatar
|
||||
*ngIf="user.human && user.human.profile && user.human.profile.displayName && user.human.profile.firstName && user.human.profile.lastName; else cog"
|
||||
class="avatar" [name]="user.human.profile.displayName"
|
||||
[avatarUrl]="user.human.profile?.avatarUrl || ''" [forColor]="user.preferredLoginName" [size]="32">
|
||||
</cnsl-avatar>
|
||||
<ng-template #cog>
|
||||
<cnsl-avatar class="avatar" [forColor]="user.preferredLoginName" [isMachine]="true" [size]="32">
|
||||
</cnsl-avatar>
|
||||
</ng-template>
|
||||
</div>
|
||||
<div class="user-option-column">
|
||||
<span>{{(user.human && user.human.profile && user.human.profile.displayName) ?
|
||||
user.human.profile.displayName :
|
||||
user.machine?.name}}</span>
|
||||
<span class="fill-space"></span>
|
||||
<span class="smaller cnsl-secondary-text">{{user.preferredLoginName}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</mat-option>
|
||||
</mat-autocomplete>
|
||||
|
||||
<span class="target-desc">
|
||||
{{'USER.TARGET.SELF'| translate}}
|
||||
<a (click)="changeTarget()">{{'USER.TARGET.CLICKHERE' | translate}}</a>
|
||||
{{'USER.TARGET.SELF'| translate}}
|
||||
<a (click)="changeTarget()">{{'USER.TARGET.CLICKHERE' | translate}}</a>
|
||||
</span>
|
||||
</cnsl-form-field>
|
||||
|
||||
<div *ngIf="target && target === UserTarget.EXTERNAL" class="line">
|
||||
<cnsl-form-field class="form-field" appearance="outline">
|
||||
<cnsl-label>Global User Loginname</cnsl-label>
|
||||
<input cnslInput type="text" [formControl]="globalLoginNameControl" />
|
||||
<span class="target-desc">
|
||||
{{(target === UserTarget.SELF ? 'USER.TARGET.SELF' : 'USER.TARGET.EXTERNAL') | translate}}
|
||||
<a (click)="changeTarget()">{{'USER.TARGET.CLICKHERE' | translate}}</a>
|
||||
</span>
|
||||
</cnsl-form-field>
|
||||
|
||||
<button color="primary" mat-icon-button (click)="getGlobalUser()">
|
||||
<i class="las la-search"></i>
|
||||
</button>
|
||||
</cnsl-form-field>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<div class="found" *ngIf="target === UserTarget.EXTERNAL && users.length > 0">
|
||||
<span class="found-label">{{'USER.SEARCH.FOUND' | translate}}:</span>
|
||||
<div class="found-user-row" *ngFor="let user of users; index as i">
|
||||
<div class="circle">
|
||||
<cnsl-avatar
|
||||
*ngIf="user.human && user.human.profile && user.human.profile.displayName && user.human.profile.firstName && user.human.profile.lastName; else cog"
|
||||
class="avatar" [name]="user.human.profile.displayName" [avatarUrl]="user.human.profile?.avatarUrl || ''" [forColor]="user.preferredLoginName ?? 'A'" [size]="32">
|
||||
</cnsl-avatar>
|
||||
<ng-template #cog>
|
||||
<div class="sa-icon">
|
||||
<i class="las la-user-cog"></i>
|
||||
</div>
|
||||
</ng-template>
|
||||
</div>
|
||||
<ng-container *ngIf="target && target === UserTarget.EXTERNAL">
|
||||
<div class="line">
|
||||
<cnsl-form-field class="user-create-form-field more-space">
|
||||
<cnsl-label>{{'USER.SEARCH.ADDITIONAL-EXTERNAL' | translate}}</cnsl-label>
|
||||
<input cnslInput type="text" [formControl]="globalLoginNameControl" placeholder="[email protected]" />
|
||||
<span class="target-desc">
|
||||
{{(target === UserTarget.SELF ? 'USER.TARGET.SELF' : 'USER.TARGET.EXTERNAL') | translate}}
|
||||
<a (click)="changeTarget()">{{'USER.TARGET.CLICKHERE' | translate}}</a>
|
||||
</span>
|
||||
</cnsl-form-field>
|
||||
|
||||
<span>{{user.preferredLoginName}}</span>
|
||||
<button mat-icon-button color="warn">
|
||||
<i class="las la-minus-circle" (click)="users.splice(i, 1)"></i>
|
||||
</button>
|
||||
</div>
|
||||
<button color="primary" mat-icon-button (click)="getGlobalUser()">
|
||||
<mat-icon>add</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</ng-container>
|
||||
</form>
|
||||
+70
-23
@@ -1,14 +1,7 @@
|
||||
.full-width {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.more-space {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.target-desc {
|
||||
font-size: 14px;
|
||||
display: block;
|
||||
margin-top: 0.5rem;
|
||||
|
||||
a {
|
||||
color: #4072b4;
|
||||
@@ -21,13 +14,17 @@
|
||||
}
|
||||
}
|
||||
|
||||
.line {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
.user-create-form-field {
|
||||
flex: 1;
|
||||
|
||||
.form-field {
|
||||
flex: 1;
|
||||
&.more-space {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.line {
|
||||
display: flex;
|
||||
max-width: 500px;
|
||||
|
||||
button {
|
||||
margin-top: 30px;
|
||||
@@ -36,20 +33,24 @@
|
||||
|
||||
.sm-dlt {
|
||||
cursor: pointer;
|
||||
font-size: .8rem;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.found {
|
||||
margin: .5rem 0;
|
||||
background: #4072b410;
|
||||
border-radius: .5rem;
|
||||
padding: .5rem;
|
||||
margin: 0.5rem 0;
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.5rem 0;
|
||||
max-width: 500px;
|
||||
box-sizing: border-box;
|
||||
|
||||
.add-user-row {
|
||||
margin-left: -4px;
|
||||
}
|
||||
|
||||
.found-user-row {
|
||||
padding: .5rem 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 56px;
|
||||
padding: 0.25rem 0;
|
||||
|
||||
button {
|
||||
visibility: hidden;
|
||||
@@ -63,16 +64,62 @@
|
||||
}
|
||||
|
||||
.found-label {
|
||||
font-size: .9rem;
|
||||
color: var(--grey);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.user-name-column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
||||
.smaller {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
.fill-space {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.search-user-dl-btn {
|
||||
transition: color 0.2s ease;
|
||||
|
||||
&:not(:hover) {
|
||||
color: var(--grey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.circle {
|
||||
margin-right: .5rem;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
input {
|
||||
max-width: 500px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.user-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.user-option-column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
|
||||
span {
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
.fill-space {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.smaller {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+46
-40
@@ -17,7 +17,7 @@ import { from, of, Subject } from 'rxjs';
|
||||
import { debounceTime, switchMap, takeUntil, tap } from 'rxjs/operators';
|
||||
import { ListUsersResponse } from 'src/app/proto/generated/zitadel/management_pb';
|
||||
import { TextQueryMethod } from 'src/app/proto/generated/zitadel/object_pb';
|
||||
import { SearchQuery, User, UserNameQuery } from 'src/app/proto/generated/zitadel/user_pb';
|
||||
import { DisplayNameQuery, SearchQuery, User } from 'src/app/proto/generated/zitadel/user_pb';
|
||||
import { ManagementService } from 'src/app/services/mgmt.service';
|
||||
import { ToastService } from 'src/app/services/toast.service';
|
||||
|
||||
@@ -42,6 +42,7 @@ export class SearchUserAutocompleteComponent implements OnInit, AfterContentChec
|
||||
|
||||
public loginNames: string[] = [];
|
||||
@Input() public users: Array<User.AsObject> = [];
|
||||
@Input() public editState: boolean = true;
|
||||
public filteredUsers: Array<User.AsObject> = [];
|
||||
public isLoading: boolean = false;
|
||||
@Input() public target: UserTarget = UserTarget.SELF;
|
||||
@@ -49,11 +50,11 @@ export class SearchUserAutocompleteComponent implements OnInit, AfterContentChec
|
||||
public UserTarget: any = UserTarget;
|
||||
@ViewChild('usernameInput') public usernameInput!: ElementRef<HTMLInputElement>;
|
||||
@ViewChild('auto') public matAutocomplete!: MatAutocomplete;
|
||||
@Output() public selectionChanged: EventEmitter<User.AsObject | User.AsObject[]> = new EventEmitter();
|
||||
@Output() public selectionChanged: EventEmitter<User.AsObject[]> = new EventEmitter();
|
||||
@Input() public singleOutput: boolean = false;
|
||||
|
||||
private unsubscribed$: Subject<void> = new Subject();
|
||||
constructor(private userService: ManagementService, private toast: ToastService, private cdref: ChangeDetectorRef) { }
|
||||
constructor(private userService: ManagementService, private toast: ToastService, private cdref: ChangeDetectorRef) {}
|
||||
|
||||
public ngOnInit(): void {
|
||||
if (this.target === UserTarget.EXTERNAL) {
|
||||
@@ -69,30 +70,33 @@ export class SearchUserAutocompleteComponent implements OnInit, AfterContentChec
|
||||
}
|
||||
|
||||
private getFilteredResults(): void {
|
||||
this.myControl.valueChanges.pipe(debounceTime(200),
|
||||
takeUntil(this.unsubscribed$),
|
||||
tap(() => this.isLoading = true),
|
||||
switchMap(value => {
|
||||
const query = new SearchQuery();
|
||||
this.myControl.valueChanges
|
||||
.pipe(
|
||||
debounceTime(200),
|
||||
takeUntil(this.unsubscribed$),
|
||||
tap(() => (this.isLoading = true)),
|
||||
switchMap((value) => {
|
||||
const query = new SearchQuery();
|
||||
|
||||
const unQuery = new UserNameQuery();
|
||||
unQuery.setMethod(TextQueryMethod.TEXT_QUERY_METHOD_CONTAINS_IGNORE_CASE);
|
||||
unQuery.setUserName(value);
|
||||
const dnQuery = new DisplayNameQuery();
|
||||
dnQuery.setMethod(TextQueryMethod.TEXT_QUERY_METHOD_CONTAINS_IGNORE_CASE);
|
||||
dnQuery.setDisplayName(value);
|
||||
|
||||
query.setUserNameQuery(unQuery);
|
||||
query.setDisplayNameQuery(dnQuery);
|
||||
|
||||
if (this.target === UserTarget.SELF) {
|
||||
return from(this.userService.listUsers(10, 0, [query]));
|
||||
} else {
|
||||
return of();
|
||||
if (this.target === UserTarget.SELF) {
|
||||
return from(this.userService.listUsers(10, 0, [query]));
|
||||
} else {
|
||||
return of();
|
||||
}
|
||||
}),
|
||||
)
|
||||
.subscribe((userresp: ListUsersResponse.AsObject | unknown) => {
|
||||
this.isLoading = false;
|
||||
if (this.target === UserTarget.SELF && userresp) {
|
||||
this.filteredUsers = (userresp as ListUsersResponse.AsObject).resultList;
|
||||
}
|
||||
}),
|
||||
).subscribe((userresp: ListUsersResponse.AsObject | unknown) => {
|
||||
this.isLoading = false;
|
||||
if (this.target === UserTarget.SELF && userresp) {
|
||||
this.filteredUsers = (userresp as ListUsersResponse.AsObject).resultList;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public displayFn(user?: User.AsObject): string {
|
||||
@@ -115,8 +119,10 @@ export class SearchUserAutocompleteComponent implements OnInit, AfterContentChec
|
||||
if (index > -1) {
|
||||
if (this.users && this.users.length > 0) {
|
||||
this.users.push(this.filteredUsers[index]);
|
||||
this.selectionChanged.emit(this.users);
|
||||
} else {
|
||||
this.users = [this.filteredUsers[index]];
|
||||
this.selectionChanged.emit(this.users);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -140,7 +146,7 @@ export class SearchUserAutocompleteComponent implements OnInit, AfterContentChec
|
||||
const index = this.filteredUsers.findIndex((user) => user === event.option.value);
|
||||
if (index !== -1) {
|
||||
if (this.singleOutput) {
|
||||
this.selectionChanged.emit(this.filteredUsers[index]);
|
||||
this.selectionChanged.emit([this.filteredUsers[index]]);
|
||||
} else {
|
||||
if (this.users && this.users.length > 0) {
|
||||
this.users.push(this.filteredUsers[index]);
|
||||
@@ -148,11 +154,8 @@ export class SearchUserAutocompleteComponent implements OnInit, AfterContentChec
|
||||
this.users = [this.filteredUsers[index]];
|
||||
}
|
||||
|
||||
if (this.singleOutput) {
|
||||
this.selectionChanged.emit(this.users[0]);
|
||||
} else {
|
||||
this.selectionChanged.emit(this.users);
|
||||
}
|
||||
this.selectionChanged.emit(this.users);
|
||||
|
||||
this.usernameInput.nativeElement.value = '';
|
||||
this.myControl.setValue(null);
|
||||
}
|
||||
@@ -171,16 +174,19 @@ export class SearchUserAutocompleteComponent implements OnInit, AfterContentChec
|
||||
}
|
||||
|
||||
public getGlobalUser(): void {
|
||||
this.userService.getUserByLoginNameGlobal(this.globalLoginNameControl.value).then(resp => {
|
||||
if (this.singleOutput && resp.user) {
|
||||
this.users = [resp.user];
|
||||
this.selectionChanged.emit(this.users[0]);
|
||||
} else if (resp.user) {
|
||||
this.users.push(resp.user);
|
||||
this.selectionChanged.emit(this.users);
|
||||
}
|
||||
}).catch(error => {
|
||||
this.toast.showError(error);
|
||||
});
|
||||
this.userService
|
||||
.getUserByLoginNameGlobal(this.globalLoginNameControl.value)
|
||||
.then((resp) => {
|
||||
if (this.singleOutput && resp.user) {
|
||||
this.users = [resp.user];
|
||||
this.selectionChanged.emit([this.users[0]]);
|
||||
} else if (resp.user) {
|
||||
this.users.push(resp.user);
|
||||
this.selectionChanged.emit(this.users);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+19
-18
@@ -7,29 +7,30 @@ import { MatChipsModule } from '@angular/material/chips';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
|
||||
import { AvatarModule } from '../avatar/avatar.module';
|
||||
import { InputModule } from '../input/input.module';
|
||||
import { SearchUserAutocompleteComponent } from './search-user-autocomplete.component';
|
||||
|
||||
|
||||
@NgModule({
|
||||
declarations: [SearchUserAutocompleteComponent],
|
||||
imports: [
|
||||
CommonModule,
|
||||
MatAutocompleteModule,
|
||||
MatChipsModule,
|
||||
MatButtonModule,
|
||||
InputModule,
|
||||
MatIconModule,
|
||||
ReactiveFormsModule,
|
||||
MatProgressSpinnerModule,
|
||||
FormsModule,
|
||||
TranslateModule,
|
||||
MatSelectModule,
|
||||
AvatarModule,
|
||||
],
|
||||
exports: [SearchUserAutocompleteComponent],
|
||||
declarations: [SearchUserAutocompleteComponent],
|
||||
imports: [
|
||||
CommonModule,
|
||||
MatAutocompleteModule,
|
||||
MatChipsModule,
|
||||
MatButtonModule,
|
||||
InputModule,
|
||||
MatIconModule,
|
||||
ReactiveFormsModule,
|
||||
MatProgressSpinnerModule,
|
||||
FormsModule,
|
||||
MatTooltipModule,
|
||||
TranslateModule,
|
||||
MatSelectModule,
|
||||
AvatarModule,
|
||||
],
|
||||
exports: [SearchUserAutocompleteComponent],
|
||||
})
|
||||
export class SearchUserAutocompleteModule { }
|
||||
export class SearchUserAutocompleteModule {}
|
||||
|
||||
Reference in New Issue
Block a user