mirror of
https://github.com/andwati/zitadel.git
synced 2026-11-12 09:05:12 +00:00
feat(console): events (#5092)
Shows the list of events on instance level
This commit is contained in:
@@ -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 {}
|
||||
Reference in New Issue
Block a user