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,45 +1,44 @@
<div class="change-header">
<span class="ch-header">{{ 'CHANGES.LISTTITLE' | translate }}</span>
<button matTooltip="{{'ACTIONS.REFRESH' | translate}}" (click)="init()" mat-icon-button>
<mat-icon class="icon">refresh</mat-icon>
</button>
<span class="ch-header">{{ 'CHANGES.LISTTITLE' | translate }}</span>
</div>
<div class="scroll-container" cnslScrollable (scrollPosition)="scrollHandler($event)">
<li class="item change-item-back" *ngFor="let hist of data | async; index as histindex">
<span *ngIf="hist.values[0].dates[0]" class="date">
{{ hist.values[0].dates[0]| timestampToDate | localizedDate: 'dd. MMMM YYYY' }}
</span>
<div class="item" *ngFor="let dayelement of hist.values; index as i">
<div class="row">
<cnsl-avatar matTooltip="{{ dayelement.editorDisplayName }}"
*ngIf="dayelement.editorDisplayName; else spacer" class="avatar"
[name]="dayelement.editorDisplayName" [size]="32" [forColor]="dayelement?.editorPreferredLoginName ?? 'A'"
[avatarUrl]="dayelement.editorAvatarUrl || ''">
</cnsl-avatar>
<ng-template #spacer>
<div class="spacer"></div>
</ng-template>
<div class="actions">
<div class="action" *ngFor="let action of dayelement.eventTypes; index as j">
<button disabled mat-icon-button aria-label="Restore history"
matTooltip="{{ dayelement.dates[j] | timestampToDate | localizedDate: 'dd. MMMM YYYY, HH:mm' }}">
<mat-icon class="icon">schedule</mat-icon>
</button>
<span>
<span class="msg">{{ action.localizedMessage }}</span>
<span class="block">{{
dayelement.dates[j] | timestampToDate | localizedDate: 'HH:mm'
}}</span>
</span>
</div>
</div>
<div class="scroll-container">
<li class="item" *ngFor="let hist of data | async; index as histindex">
<span *ngIf="hist.values[0].dates[0]" class="date">
{{ hist.values[0].dates[0]| timestampToDate | localizedDate: 'dd. MMM YYYY' }}
</span>
<div class="item" *ngFor="let dayelement of hist.values; index as i">
<div class="row">
<cnsl-avatar matTooltip="{{ dayelement.editorDisplayName }}" *ngIf="dayelement.editorDisplayName; else spacer"
class="avatar" [name]="dayelement.editorDisplayName" [size]="32"
[forColor]="dayelement?.editorPreferredLoginName ?? 'A'" [avatarUrl]="dayelement.editorAvatarUrl || ''">
</cnsl-avatar>
<ng-template #spacer>
<div class="spacer"></div>
</ng-template>
<div class="change-actions">
<div class="change-action" *ngFor="let action of dayelement.eventTypes; index as j">
<div>
<span class="msg">{{ action.localizedMessage }}</span>
<span class="block"
matTooltip="{{ dayelement.dates[j] | timestampToDate | localizedDate: 'dd. MM YYYY, HH:mm' }}">{{
dayelement.dates[j] | timestampToDate | localizedDate: 'HH:mm'
}}</span>
</div>
</div>
</div>
</li>
<div class="sp-wrapper">
<mat-spinner *ngIf="loading | async" diameter="25"></mat-spinner>
</div>
</div>
<span class="end-container" *ngIf="bottom">{{'CHANGES.BOTTOM' | translate}}</span>
</div>
</li>
<div class="ch-sp-wrapper">
<mat-spinner *ngIf="loading | async" diameter="25"></mat-spinner>
</div>
<button (click)="more()" class="load-more-button" *ngIf="!bottom" mat-stroked-button>
{{'CHANGES.LOADMORE' | translate}}
</button>
<div *ngIf="bottom" class="end-container">
<span class="cnsl-secondary-text">{{'CHANGES.BOTTOM' | translate}}</span>
</div>
</div>
@@ -11,7 +11,7 @@
font-weight: 400;
margin-top: 1rem;
font-size: 14px;
letter-spacing: .05em;
letter-spacing: 0.05em;
text-transform: uppercase;
}
@@ -23,24 +23,25 @@
@mixin changes-theme($theme) {
$is-dark-theme: map-get($theme, is-dark);
$foreground: map-get($theme, foreground);
$secondary-text: map-get($foreground, secondary-text);
.scroll-container {
max-height: 50vh;
overflow-y: scroll;
border-bottom: 1px solid map-get($foreground, divider);
margin-bottom: .5rem;
margin-bottom: 0.5rem;
box-sizing: border-box;
.date {
font-weight: 500;
font-size: .8rem;
font-size: 0.8rem;
display: block;
margin-bottom: .5rem;
margin-bottom: 0.5rem;
}
.item {
display: block;
padding: 10px 0;
font-size: .8rem;
font-size: 0.8rem;
box-sizing: border-box;
.row {
display: flex;
@@ -50,31 +51,25 @@
width: 32px;
}
.actions {
.change-actions {
flex: 1;
display: flex;
flex-direction: column;
margin-top: -.5rem;
margin-top: -0.25rem;
margin-left: 1rem;
.action {
.change-action {
display: flex;
flex-direction: row;
align-items: center;
flex: 1;
.icon {
width: 32px;
display: inline-block;
height: 1.2rem;
line-height: 1.2rem;
font-size: 1.2rem;
color: var(--grey);
}
padding: 0.25rem 0;
cursor: default;
span {
flex: 1;
font-weight: 500;
font-size: .8rem;
font-size: 0.8rem;
overflow-x: hidden;
}
@@ -84,60 +79,32 @@
.block {
display: block;
}
.restore {
visibility: hidden;
display: none;
opacity: 0;
margin-left: 1rem;
transform: opacity .2s ease-in-out;
}
&:hover {
.restore {
visibility: visible;
display: inline-block;
opacity: 1;
color: #81868a;
&[disabled] {
visibility: hidden;
}
&:hover {
color: white;
}
}
color: $secondary-text;
}
}
}
/* stylelint-disable */
$primary: map-get($theme, primary);
$is-dark-theme: map-get($theme, is-dark);
$transparent-color: if($is-dark-theme, rgba(#8795a1, .2), rgba(#8795a1, .2));
/* stylelint-enable */
&.change-item-back {
background-color: rgba($transparent-color, .93);
transition: background-color .3s cubic-bezier(.645, .045, .355, 1);
}
}
}
.sp-wrapper {
padding: .5rem;
display: flex;
justify-content: center;
}
.ch-sp-wrapper {
padding: 0.5rem;
display: flex;
justify-content: center;
box-sizing: border-box;
}
.load-more-button {
margin-bottom: 1rem;
}
.end-container {
font-size: 14px;
margin: 1rem 0 1rem 0;
display: block;
color: var(--grey);
display: flex;
flex-direction: column;
span {
font-size: 13px;
}
}
}
}
@@ -32,16 +32,17 @@ export interface MappedChange {
editorDisplayName: string;
editorAvatarUrl: string;
editorPreferredLoginName: string;
eventTypes: Array<{ key: string; localizedMessage: string; }>;
eventTypes: Array<{ key: string; localizedMessage: string }>;
sequences: number[];
}>;
}
type ListChanges = ListMyUserChangesResponse.AsObject |
ListUserChangesResponse.AsObject |
ListProjectChangesResponse.AsObject |
ListOrgChangesResponse.AsObject |
ListAppChangesResponse.AsObject;
type ListChanges =
| ListMyUserChangesResponse.AsObject
| ListUserChangesResponse.AsObject
| ListProjectChangesResponse.AsObject
| ListOrgChangesResponse.AsObject
| ListAppChangesResponse.AsObject;
@Component({
selector: 'cnsl-changes',
@@ -64,9 +65,7 @@ export class ChangesComponent implements OnInit, OnDestroy {
public data!: Observable<MappedChange[]>;
public changes!: ListChanges;
private destroyed$: Subject<void> = new Subject();
constructor(private mgmtUserService: ManagementService, private authUserService: GrpcAuthService) {
}
constructor(private mgmtUserService: ManagementService, private authUserService: GrpcAuthService) {}
ngOnInit(): void {
this.init();
@@ -82,24 +81,23 @@ export class ChangesComponent implements OnInit, OnDestroy {
this.destroyed$.next();
}
public scrollHandler(e: any): void {
if (e === 'bottom') {
this.more();
}
}
public init(): void {
let first: Promise<ListChanges>;
switch (this.changeType) {
case ChangeType.MYUSER: first = this.authUserService.listMyUserChanges(20, 0);
case ChangeType.MYUSER:
first = this.authUserService.listMyUserChanges(30, 0);
break;
case ChangeType.USER: first = this.mgmtUserService.listUserChanges(this.id, 20, 0);
case ChangeType.USER:
first = this.mgmtUserService.listUserChanges(this.id, 30, 0);
break;
case ChangeType.PROJECT: first = this.mgmtUserService.listProjectChanges(this.id, 20, 0);
case ChangeType.PROJECT:
first = this.mgmtUserService.listProjectChanges(this.id, 30, 0);
break;
case ChangeType.ORG: first = this.mgmtUserService.listOrgChanges(20, 0);
case ChangeType.ORG:
first = this.mgmtUserService.listOrgChanges(30, 0);
break;
case ChangeType.APP: first = this.mgmtUserService.listAppChanges(this.id, this.secId, 20, 0);
case ChangeType.APP:
first = this.mgmtUserService.listAppChanges(this.id, this.secId, 30, 0);
break;
}
@@ -109,24 +107,30 @@ export class ChangesComponent implements OnInit, OnDestroy {
this.data = this._data.asObservable().pipe(
scan((acc, val) => {
return false ? val.concat(acc) : acc.concat(val);
}));
}),
);
}
private more(): void {
public more(): void {
const cursor = this.getCursor();
let more: Promise<ListChanges>;
switch (this.changeType) {
case ChangeType.MYUSER: more = this.authUserService.listMyUserChanges(20, cursor);
case ChangeType.MYUSER:
more = this.authUserService.listMyUserChanges(20, cursor);
break;
case ChangeType.USER: more = this.mgmtUserService.listUserChanges(this.id, 20, cursor);
case ChangeType.USER:
more = this.mgmtUserService.listUserChanges(this.id, 20, cursor);
break;
case ChangeType.PROJECT: more = this.mgmtUserService.listProjectChanges(this.id, 20, cursor);
case ChangeType.PROJECT:
more = this.mgmtUserService.listProjectChanges(this.id, 20, cursor);
break;
case ChangeType.ORG: more = this.mgmtUserService.listOrgChanges(20, cursor);
case ChangeType.ORG:
more = this.mgmtUserService.listOrgChanges(20, cursor);
break;
case ChangeType.APP: more = this.mgmtUserService.listAppChanges(this.id, this.secId, 20, cursor);
case ChangeType.APP:
more = this.mgmtUserService.listAppChanges(this.id, this.secId, 20, cursor);
break;
}
@@ -147,42 +151,45 @@ export class ChangesComponent implements OnInit, OnDestroy {
// Maps the snapshot to usable format the updates source
private mapAndUpdate(col: Promise<ListChanges>): any {
if (this._done.value || this._loading.value) { return; }
if (this._done.value || this._loading.value) {
return;
}
// Map snapshot with doc ref (needed for cursor)
if (!this.bottom) {
// loading
this._loading.next(true);
return from(col).pipe(
take(1),
tap((res: ListChanges) => {
const values = res.resultList;
const mapped = this.mapChanges(values);
// update source with new values, done loading
// this._data.next(values);
this._data.next(mapped);
return from(col)
.pipe(
take(1),
tap((res: ListChanges) => {
const values = res.resultList;
const mapped = this.mapChanges(values);
this._loading.next(false);
this._data.next(mapped);
// no more values, mark done
if (!values.length) {
this._done.next(true);
}
}),
catchError(_ => {
this._loading.next(false);
this.bottom = true;
return of([]);
}),
).subscribe();
this._loading.next(false);
if (!values.length) {
this._done.next(true);
}
}),
catchError((_) => {
this._loading.next(false);
this.bottom = true;
return of([]);
}),
)
.subscribe();
}
}
private mapChanges(changes: Change.AsObject[]): {
key: string; values: any[];
key: string;
values: any[];
}[] {
const splitted: { [editorId: string]: any[]; } = {};
const splitted: { [editorId: string]: any[] } = {};
changes.forEach((change) => {
if (change.changeDate) {
const index = `${this.getDateString(change.changeDate)}`;
@@ -230,7 +237,7 @@ export class ChangesComponent implements OnInit, OnDestroy {
}
}
});
const arr = Object.keys(splitted).map(key => {
const arr = Object.keys(splitted).map((key) => {
return { key: key, values: splitted[key] };
});
@@ -263,7 +270,7 @@ export class ChangesComponent implements OnInit, OnDestroy {
// Order by descending property key
keyDescOrder = (a: KeyValue<number, string>, b: KeyValue<number, string>): number => {
return a.key > b.key ? -1 : (b.key > a.key ? 1 : 0);
return a.key > b.key ? -1 : b.key > a.key ? 1 : 0;
};
/* eslint-enable */
}