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:
Max Peintner
2022-04-28 10:35:02 +00:00
committed by GitHub
co-authored by Livio Amstutz
parent 00f7dbe875
commit 08ae39ae19
668 changed files with 47746 additions and 19117 deletions
@@ -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>
@@ -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;
}
}
}
@@ -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);
});
}
}
@@ -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 {}