feat(console): events (#5092)

Shows the list of events on instance level
This commit is contained in:
Max Peintner
2023-02-01 08:54:00 +00:00
committed by GitHub
parent 5ca7e83f0a
commit e9d5d1dcaf
35 changed files with 1790 additions and 13 deletions
@@ -0,0 +1,17 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { EventsComponent } from './events.component';
const routes: Routes = [
{
path: '',
component: EventsComponent,
},
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class EventsRoutingModule {}
@@ -0,0 +1,129 @@
<div class="max-width-container">
<h1 class="events-title">{{ 'IAM.EVENTS.TITLE' | translate }}</h1>
<p class="events-desc cnsl-secondary-text">{{ 'IAM.EVENTS.DESCRIPTION' | translate }}</p>
<cnsl-refresh-table
[hideRefresh]="true"
(refreshed)="refresh()"
[dataSize]="dataSource.data.length"
[loading]="_loading | async"
>
<div actions>
<cnsl-filter-events (requestChanged)="filterChanged($event)"></cnsl-filter-events>
</div>
<table
[dataSource]="dataSource"
mat-table
class="table views-table"
aria-label="Views"
matSort
(matSortChange)="sortChange($event)"
>
<ng-container matColumnDef="editor">
<th mat-header-cell *matHeaderCellDef>{{ 'IAM.EVENTS.EDITOR' | translate }}</th>
<td mat-cell *matCellDef="let event">
<ng-container *ngIf="event | toobject as event">
<div class="editor-row" *ngIf="event.editor as editor">
<!-- <cnsl-avatar
*ngIf="editor && editor.displayName; else cog"
class="avatar"
[name]="editor.displayName"
[avatarUrl]="editor.avatarUrl || ''"
[forColor]="editor.preferredLoginName ?? editor.displayName"
[size]="32"
>
</cnsl-avatar>
<ng-template #cog>
<cnsl-avatar [forColor]="editor?.preferredLoginName ?? 'franz'" [isMachine]="true">
<i class="las la-robot"></i>
</cnsl-avatar> </ng-template
> -->
<span class="name" *ngIf="editor.displayName">{{ editor.displayName }}</span>
<span class="state" *ngIf="editor.service">{{ editor.service }}</span>
</div>
</ng-container>
</td>
</ng-container>
<ng-container matColumnDef="aggregate">
<th mat-header-cell *matHeaderCellDef>{{ 'IAM.EVENTS.AGGREGATE' | translate }}</th>
<td mat-cell *matCellDef="let event">
<ng-container *ngIf="event | toobject as event">
<div class="aggregate-row">
<span class="id">{{ event.aggregate.id }}</span
><span class="state" *ngIf="event.aggregate?.type?.localized?.localizedMessage">{{
event.aggregate.type.localized.localizedMessage
}}</span>
</div>
</ng-container>
</td>
</ng-container>
<ng-container matColumnDef="resourceOwner">
<th mat-header-cell *matHeaderCellDef>{{ 'IAM.EVENTS.RESOURCEOWNER' | translate }}</th>
<td mat-cell *matCellDef="let event">
<ng-container *ngIf="event | toobject as event">
<span *ngIf="event.aggregate.resourceOwner">{{ event.aggregate.resourceOwner }}</span>
</ng-container>
</td>
</ng-container>
<ng-container matColumnDef="sequence">
<th mat-header-cell *matHeaderCellDef mat-sort-header [start]="'desc'" [disableClear]="true">
{{ 'IAM.EVENTS.SEQUENCE' | translate }}
</th>
<td mat-cell *matCellDef="let event">
<ng-container *ngIf="event | toobject as event">
{{ event.sequence }}
</ng-container>
</td>
</ng-container>
<ng-container matColumnDef="creationDate">
<th mat-header-cell *matHeaderCellDef>{{ 'IAM.EVENTS.CREATIONDATE' | translate }}</th>
<td mat-cell *matCellDef="let event">
<ng-container *ngIf="event | toobject as event">
<span>{{ event?.creationDate | timestampToDate | localizedDate : 'EEE dd. MMM, HH:mm' }}</span>
</ng-container>
</td>
</ng-container>
<ng-container matColumnDef="type">
<th mat-header-cell *matHeaderCellDef>{{ 'IAM.EVENTS.TYPE' | translate }}</th>
<td mat-cell *matCellDef="let event">
<ng-container *ngIf="event | toobject as event">
<span *ngIf="event.type?.localized?.localizedMessage">{{ event.type.localized.localizedMessage }}</span>
</ng-container>
</td>
</ng-container>
<ng-container matColumnDef="payload">
<th mat-header-cell *matHeaderCellDef>{{ 'IAM.EVENTS.PAYLOAD' | translate }}</th>
<td mat-cell *matCellDef="let event">
<ng-container *ngIf="event | topayload as payload">
<span>{{ payload | json }}</span>
<div class="btn-wrapper">
<button class="open-in-dialog-btn" mat-icon-button (click)="openDialog(event)">
<mat-icon svgIcon="mdi_arrow_expand"></mat-icon>
</button>
</div>
</ng-container>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr class="highlight" mat-row *matRowDef="let row; columns: displayedColumns"></tr>
</table>
<cnsl-paginator
#paginator
class="paginator"
[hidePagination]="true"
[showMoreButton]="true"
[disableShowMore]="_done | async"
(moreRequested)="more()"
[length]="dataSource.data.length"
>
</cnsl-paginator>
</cnsl-refresh-table>
</div>
@@ -0,0 +1,97 @@
@use '@angular/material' as mat;
@mixin events-theme($theme) {
$background: map-get($theme, background);
$foreground: map-get($theme, foreground);
$is-dark-theme: map-get($theme, is-dark);
$card-background-color: mat.get-color-from-palette($background, cards);
$border-color: if($is-dark-theme, rgba(#8795a1, 0.2), rgba(#8795a1, 0.2));
$primary: map-get($theme, primary);
$primary-color: mat.get-color-from-palette($primary, 500);
.mat-column-payload {
position: relative;
white-space: nowrap;
max-width: 200px;
overflow: hidden;
text-overflow: hidden;
.btn-wrapper {
background-color: $card-background-color;
transition: background-color 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
border: 1px solid $border-color;
box-sizing: border-box;
border-radius: 0.5rem;
outline: none;
box-shadow: 0 0 3px #0000001a;
height: 36px;
width: 36px;
position: absolute;
display: none;
justify-content: center;
align-items: center;
right: 0;
top: 50%;
transform: translateY(-50%);
.open-in-dialog-btn {
width: 36px;
height: 36px;
line-height: initial;
}
}
&:hover {
.btn-wrapper {
display: flex;
}
}
}
.filter-button-wrapper {
display: flex;
justify-content: space-between;
position: relative;
user-select: none;
margin-left: 1rem;
.filter-count {
font-size: 14px;
color: $primary-color;
margin-left: 0.5rem;
}
}
}
.events-title {
margin: 2rem 0 0 0;
}
.events-desc {
font-size: 14px;
}
.editor-row {
display: flex;
flex-direction: column;
padding: 2px 0;
.name,
.id {
margin-bottom: 0.25rem;
}
.state {
align-self: flex-start;
}
}
.aggregate-row {
display: flex;
flex-direction: column;
align-items: flex-start;
.id {
margin-bottom: 0.25rem;
}
}
@@ -0,0 +1,24 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { EventsComponent } from './events.component';
describe('EventsComponent', () => {
let component: EventsComponent;
let fixture: ComponentFixture<EventsComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [EventsComponent],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(EventsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,198 @@
import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { MatSort, Sort } from '@angular/material/sort';
import { MatLegacyTableDataSource as MatTableDataSource } from '@angular/material/legacy-table';
import { BehaviorSubject, distinctUntilChanged, Observable, Subject, takeUntil } from 'rxjs';
import { ListEventsRequest, ListEventsResponse } from 'src/app/proto/generated/zitadel/admin_pb';
import { AdminService } from 'src/app/services/admin.service';
import { Breadcrumb, BreadcrumbService, BreadcrumbType } from 'src/app/services/breadcrumb.service';
import { Event } from 'src/app/proto/generated/zitadel/event_pb';
import { PaginatorComponent } from 'src/app/modules/paginator/paginator.component';
import { LiveAnnouncer } from '@angular/cdk/a11y';
import { ToastService } from 'src/app/services/toast.service';
import { DisplayJsonDialogComponent } from 'src/app/modules/display-json-dialog/display-json-dialog.component';
import { MatLegacyDialog as MatDialog } from '@angular/material/legacy-dialog';
enum EventFieldName {
EDITOR = 'editor',
AGGREGATE = 'aggregate',
RESOURCEOWNER = 'resourceOwner',
SEQUENCE = 'sequence',
CREATIONDATE = 'creationDate',
TYPE = 'type',
PAYLOAD = 'payload',
}
type LoadRequest = {
req: ListEventsRequest;
override: boolean;
};
@Component({
selector: 'cnsl-events',
templateUrl: './events.component.html',
styleUrls: ['./events.component.scss'],
})
export class EventsComponent implements OnDestroy {
public INITPAGESIZE = 20;
public sortAsc = false;
private destroy$: Subject<void> = new Subject();
public displayedColumns: string[] = [
EventFieldName.TYPE,
EventFieldName.AGGREGATE,
EventFieldName.RESOURCEOWNER,
EventFieldName.EDITOR,
EventFieldName.SEQUENCE,
EventFieldName.CREATIONDATE,
EventFieldName.PAYLOAD,
];
public currentRequest$: BehaviorSubject<LoadRequest> = new BehaviorSubject<LoadRequest>({
req: new ListEventsRequest().setLimit(this.INITPAGESIZE),
override: true,
});
@ViewChild(MatSort) public sort!: MatSort;
@ViewChild(PaginatorComponent) public paginator!: PaginatorComponent;
public dataSource: MatTableDataSource<Event> = new MatTableDataSource<Event>([]);
public _done: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
public done: Observable<boolean> = this._done.asObservable();
public _loading: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
private _data: BehaviorSubject<Event[]> = new BehaviorSubject<Event[]>([]);
constructor(
private adminService: AdminService,
private breadcrumbService: BreadcrumbService,
private _liveAnnouncer: LiveAnnouncer,
private toast: ToastService,
private dialog: MatDialog,
) {
const breadcrumbs = [
new Breadcrumb({
type: BreadcrumbType.INSTANCE,
name: 'Instance',
routerLink: ['/instance'],
}),
];
this.breadcrumbService.setBreadcrumb(breadcrumbs);
this.currentRequest$
.pipe(
// this would compare the requests if a duplicate and redundant request would be made
// distinctUntilChanged(({ req: prev }, { req: next }) => {
// return JSON.stringify(prev.toObject()) === JSON.stringify(next.toObject());
// }),
takeUntil(this.destroy$),
)
.subscribe(({ req, override }) => {
this._loading.next(true);
this.adminService
.listEvents(req)
.then((res: ListEventsResponse) => {
if (override) {
this._data = new BehaviorSubject<Event[]>([]);
this.dataSource = new MatTableDataSource<Event>([]);
}
const eventList = res.getEventsList();
this._data.next(eventList);
const concat = this.dataSource.data.concat(eventList);
this.dataSource = new MatTableDataSource<Event>(concat);
this._loading.next(false);
if (eventList.length === 0) {
this._done.next(true);
} else {
this._done.next(false);
}
})
.catch((error) => {
this.toast.showError(error);
this._loading.next(false);
this._data.next([]);
});
});
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
public loadEvents(filteredRequest: ListEventsRequest, override: boolean = false): void {
this.currentRequest$.next({ req: filteredRequest, override });
}
public refresh(): void {
const req = new ListEventsRequest();
req.setLimit(this.paginator.pageSize);
}
public sortChange(sortState: Sort) {
if (sortState.direction && sortState.active) {
this._liveAnnouncer.announce(`Sorted ${sortState.direction}ending`);
this.sortAsc = sortState.direction === 'asc';
const { req } = this.currentRequest$.value;
req.setLimit(this.INITPAGESIZE);
req.setAsc(this.sortAsc ? true : false);
this.loadEvents(req, true);
} else {
this._liveAnnouncer.announce('Sorting cleared');
}
}
public openDialog(event: Event): void {
this.dialog.open(DisplayJsonDialogComponent, {
data: {
event: event,
},
width: '450px',
});
}
public more(): void {
const sequence = this.getCursor();
const { req } = this.currentRequest$.value;
req.setSequence(sequence);
this.loadEvents(req);
}
public filterChanged(filterRequest: ListEventsRequest) {
const req = new ListEventsRequest();
req.setLimit(this.INITPAGESIZE);
req.setAsc(this.sortAsc ? true : false);
req.setAggregateTypesList(filterRequest.getAggregateTypesList());
req.setAggregateId(filterRequest.getAggregateId());
req.setEventTypesList(filterRequest.getEventTypesList());
req.setEditorUserId(filterRequest.getEditorUserId());
req.setResourceOwner(filterRequest.getResourceOwner());
req.setSequence(filterRequest.getSequence());
req.setCreationDate(filterRequest.getCreationDate());
const isAsc: boolean = filterRequest.getAsc();
req.setAsc(isAsc);
if (this.sortAsc !== isAsc) {
this.sort.sort({ id: 'sequence', start: isAsc ? 'asc' : 'desc', disableClear: true });
}
this.loadEvents(req, true);
}
private getCursor(): number {
const current = this._data.value;
if (current.length) {
const sequence = current[current.length - 1].toObject().sequence;
return sequence;
}
return 0;
}
}
@@ -0,0 +1,65 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
import { MatIconModule } from '@angular/material/icon';
import { MatLegacyProgressSpinnerModule as MatProgressSpinnerModule } from '@angular/material/legacy-progress-spinner';
import { MatSortModule } from '@angular/material/sort';
import { MatLegacyTableModule as MatTableModule } from '@angular/material/legacy-table';
import { MatLegacyTooltipModule as MatTooltipModule } from '@angular/material/legacy-tooltip';
import { TranslateModule } from '@ngx-translate/core';
import { CopyToClipboardModule } from 'src/app/directives/copy-to-clipboard/copy-to-clipboard.module';
import { CardModule } from 'src/app/modules/card/card.module';
import { InfoSectionModule } from 'src/app/modules/info-section/info-section.module';
import { InputModule } from 'src/app/modules/input/input.module';
import { PaginatorModule } from 'src/app/modules/paginator/paginator.module';
import { RefreshTableModule } from 'src/app/modules/refresh-table/refresh-table.module';
import { TableActionsModule } from 'src/app/modules/table-actions/table-actions.module';
import { HasRolePipeModule } from 'src/app/pipes/has-role-pipe/has-role-pipe.module';
import { LocalizedDatePipeModule } from 'src/app/pipes/localized-date-pipe/localized-date-pipe.module';
import { TimestampToDatePipeModule } from 'src/app/pipes/timestamp-to-date-pipe/timestamp-to-date-pipe.module';
import { EventsComponent } from './events.component';
import { EventsRoutingModule } from './events-routing.module';
import { FilterEventsModule } from 'src/app/modules/filter-events/filter-events.module';
import { AvatarModule } from 'src/app/modules/avatar/avatar.module';
import { OverlayModule } from '@angular/cdk/overlay';
import { ActionKeysModule } from 'src/app/modules/action-keys/action-keys.module';
import { DisplayJsonDialogModule } from 'src/app/modules/display-json-dialog/display-json-dialog.module';
import { MatLegacyDialogModule } from '@angular/material/legacy-dialog';
import { ToObjectPipeModule } from 'src/app/pipes/to-object/to-object.module';
import { ToPayloadPipeModule } from 'src/app/pipes/to-payload/to-payload.module';
@NgModule({
declarations: [EventsComponent],
imports: [
EventsRoutingModule,
CommonModule,
TableActionsModule,
MatIconModule,
CardModule,
FilterEventsModule,
ToObjectPipeModule,
ToPayloadPipeModule,
HasRolePipeModule,
MatLegacyDialogModule,
MatButtonModule,
CopyToClipboardModule,
InputModule,
TranslateModule,
InfoSectionModule,
AvatarModule,
MatTooltipModule,
MatProgressSpinnerModule,
RefreshTableModule,
ActionKeysModule,
PaginatorModule,
TimestampToDatePipeModule,
LocalizedDatePipeModule,
DisplayJsonDialogModule,
MatTableModule,
MatSortModule,
OverlayModule,
],
exports: [],
})
export default class IamViewsModule {}